Record why a task is blocked; surface only unreachable sites (#119) #134

Merged
liamjd merged 2 commits from 119-blocked-task-reason into main 2026-09-27 22:27:27 +01:00
Owner

Gives a blocked task a reason, so the player is warned about a site no pawn can reach but not about a building that is simply waiting on its own sub-tasks.

Changes

  • Task.BlockReason {NONE, NOT_READY, WAITING_ON_CHILDREN, UNREACHABLE} and Task.block_reason.
  • Task.evaluate_block(ctx): a pure query. Precedence is UNREACHABLE > WAITING_ON_CHILDREN > NOT_READY. can_start() is now evaluate_block() == NONE, so complete()'s parent re-check is unchanged.
  • TaskManager.next_for() calls evaluate_block() once per task and is the only writer of block_reason. It emits task_blocked once when a task enters BLOCKED or its reason changes, and not on a repeat. It clears the reason when the task is offered again.
  • Buildings._task_blocked() sets construction_blocked to block_reason == UNREACHABLE, so a change of reason can clear the red as well as set it.
  • ToastNotifier.for_blocked() only returns a message for UNREACHABLE, titled "Building entrance unreachable". That title also fits demolition, which goes through the same path. _on_task_blocked() null-checks rather than repeating the rule.
  • Debug panel: task rows show the status and, when blocked, the reason, by name.
  • Outside the ticket: the debug panel's pawn rows now show the pawn state by name rather than as an int.

Decisions (recorded on #119)

  • Only UNREACHABLE is surfaced, as an allow-list. NOT_READY becomes quiet as a result; today it only arises when a task outlives its building.
  • The toast uses the default timeout, not sticky. The red building is the persistent signal. A sticky toast would outlive the problem, because nothing dismisses it when the route reopens.

Verification

  • Spec first (606e3ca): test_task_block_reason.gd (new, 11 tests), plus additions to test_buildings.gd and test_toast_notifier.gd. It was checked satisfiable against a throwaway implementation before this one was written.
  • test_a_new_block_is_announced_once was added during review, after it caught a double emit on a first block.
  • Full GUT suite: 766/767 passing, 1 existing pending (#85).
  • Checked in game with a sticky toast, then reverted to the default timeout.

Not done

  • #124 item 3: next_for() still emits task_unblocked for tasks that were never blocked, in the same branch touched here.

Closes #119

🤖 Generated with Claude Code

Gives a blocked task a reason, so the player is warned about a site no pawn can reach but not about a building that is simply waiting on its own sub-tasks. ## Changes - `Task.BlockReason {NONE, NOT_READY, WAITING_ON_CHILDREN, UNREACHABLE}` and `Task.block_reason`. - `Task.evaluate_block(ctx)`: a pure query. Precedence is `UNREACHABLE` > `WAITING_ON_CHILDREN` > `NOT_READY`. `can_start()` is now `evaluate_block() == NONE`, so `complete()`'s parent re-check is unchanged. - `TaskManager.next_for()` calls `evaluate_block()` once per task and is the only writer of `block_reason`. It emits `task_blocked` **once** when a task enters BLOCKED or its reason changes, and not on a repeat. It clears the reason when the task is offered again. - `Buildings._task_blocked()` sets `construction_blocked` to `block_reason == UNREACHABLE`, so a change of reason can clear the red as well as set it. - `ToastNotifier.for_blocked()` only returns a message for `UNREACHABLE`, titled "Building entrance unreachable". That title also fits demolition, which goes through the same path. `_on_task_blocked()` null-checks rather than repeating the rule. - Debug panel: task rows show the status and, when blocked, the reason, by name. - **Outside the ticket:** the debug panel's pawn rows now show the pawn state by name rather than as an int. ## Decisions (recorded on #119) - Only `UNREACHABLE` is surfaced, as an allow-list. `NOT_READY` becomes quiet as a result; today it only arises when a task outlives its building. - The toast uses the default timeout, not sticky. The red building is the persistent signal. A sticky toast would outlive the problem, because nothing dismisses it when the route reopens. ## Verification - Spec first (`606e3ca`): `test_task_block_reason.gd` (new, 11 tests), plus additions to `test_buildings.gd` and `test_toast_notifier.gd`. It was checked satisfiable against a throwaway implementation before this one was written. - `test_a_new_block_is_announced_once` was added during review, after it caught a double emit on a first block. - Full GUT suite: 766/767 passing, 1 existing pending (#85). - Checked in game with a sticky toast, then reverted to the default timeout. ## Not done - #124 item 3: `next_for()` still emits `task_unblocked` for tasks that were never blocked, in the same branch touched here. Closes #119 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Tests only; the implementation is still to be written. They pin:

- Task.BlockReason {NONE, UNREACHABLE, WAITING_ON_CHILDREN, NOT_READY}
  and Task.evaluate_block(ctx), a pure query with UNREACHABLE taking
  precedence. can_start() is evaluate_block() == NONE.
- next_for() recording the reason with the block, clearing it when the
  task is offered again, and re-emitting task_blocked on a change of
  reason, but not on a repeat.
- Only UNREACHABLE marks a building or raises a toast. The notifier's
  blocked-task fixture now carries that reason.

Checked against a throwaway implementation (all 765 passing), then
discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Task gains BlockReason {NONE, NOT_READY, WAITING_ON_CHILDREN, UNREACHABLE}
and a block_reason field. evaluate_block(ctx) is the pure query, with
UNREACHABLE taking precedence; can_start() is now evaluate_block() == NONE.

next_for() calls evaluate_block() once per task and is the only writer of
block_reason. It emits task_blocked once when a task enters BLOCKED or its
reason changes, and clears the reason when the task is offered again.

Only UNREACHABLE is surfaced. Buildings sets construction_blocked from the
reason, so a change of reason can clear the red as well as set it, and
ToastNotifier.for_blocked() returns null for the quiet reasons. The debug
panel shows task status and reason by name, and pawn state by name.

Adds test_a_new_block_is_announced_once, which caught a double emit on a
first block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
liamjd merged commit 5610b09e9f into main 2026-09-27 22:27:27 +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!134
No description provided.