Skip to content

fix(#812): direction-aware signal display for DynamicRailSemaphore - #813

Merged
bedaHovorka merged 3 commits into
goal-10from
copilot/fix-dynamic-rail-semaphore-display
Jul 28, 2026
Merged

fix(#812): direction-aware signal display for DynamicRailSemaphore#813
bedaHovorka merged 3 commits into
goal-10from
copilot/fix-dynamic-rail-semaphore-display

Conversation

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

DynamicRailSemaphore.signal is a single field — not keyed by direction or train. Once any train's forward-direction reservation lights a semaphore FREE, all consumers (canvas renderers, LLM dispatcher's all_signal_aspects) see proceed regardless of which direction they're asking about. The movement-safety gap was closed in 28e3389 via ownership-checked wakeup in Train.kt; this PR fixes the perception/rendering truthfulness side.

Core change

New isAllowingFor(from, to) on DynamicRailSemaphore:

fun isAllowingFor(from: Cell.Segment?, to: Cell.Segment?): Boolean {
    if (!signal.isAllowing()) return false
    val d = staticRef.direction()
    return to == d && from == anti(d)
}

Returns true only when the signal is allowing and the queried direction matches the canonical forward direction (anti(direction()) → direction()).

Consumer updates

  • PerceptionMapping.separatorAspect — replaces raw signal read with isAllowingFor(anti(d), d). Train reactive decisions now see STOP when the semaphore is lit for the opposite direction.
  • AnimationStateCapture.captureSignalState — same guard; canvas animation no longer paints proceed for a semaphore authorized by an opposite-direction reservation.
  • DefaultNetworkPerceptionPort — adds private DynamicRailSemaphore.toReading() extension that populates two new SemaphoreReading fields.

LLM dispatcher perception

SemaphoreReading gains two nullable direction fields (backward-compatible defaults null):

data class SemaphoreReading(
    val name: String,
    val signal: Signal,
    val authorizedFrom: String? = null,   // e.g. "F" — populated only when allowing
    val authorizedTo: String? = null      // e.g. "A"
)

allSignalAspects / signalAspect now include which direction the proceed is authorized for, so the LLM dispatcher can distinguish "clear for me" from "clear for the other train."

Tests

  • 6 new isAllowingFor tests in DynamicRailSemaphoreTest (STOP, forward, reverse, after cancel, null segments, second orientation).
  • DefaultNetworkPerceptionPortTest semaphore mock updated to stub direction() and isAllowingFor; 6 SemaphoreReading assertions updated to expect the new direction fields.

Copilot AI changed the title [WIP] Fix DynamicRailSemaphore signal display for direction sensitivity fix(#812): direction-aware signal display for DynamicRailSemaphore Jul 27, 2026
Copilot AI requested a review from bedaHovorka July 27, 2026 17:42
Copilot AI added 2 commits July 28, 2026 05:50
Add isAllowingFor(from, to) to DynamicRailSemaphore, update
PerceptionMapping.separatorAspect, SemaphoreReading, DefaultNetworkPerceptionPort,
and AnimationStateCapture to use direction-aware signal checks.

- DynamicRailSemaphore.isAllowingFor(from, to): new method that returns
  true only when signal is allowing AND from/to match the canonical forward
  direction (anti(direction()) → direction()). Prevents consumers from
  treating another train's forward-direction grant as a proceed for the
  reverse direction.

- PerceptionMapping.separatorAspect: updated to call isAllowingFor(anti(d), d)
  for both DynamicRailSemaphore and DynamicInOut instead of reading raw signal.

- SemaphoreReading: added authorizedFrom/authorizedTo nullable fields (default
  null) so the LLM dispatcher's all_signal_aspects tool exposes direction context
  alongside the signal aspect.

- DefaultNetworkPerceptionPort.toReading(): populates authorizedFrom/authorizedTo
  from direction() when signal is allowing; null otherwise.

- AnimationStateCapture.captureSignalState(): uses isAllowingFor to only
  report proceed aspects authorized for the forward direction.

- DynamicRailSemaphoreTest: 6 new tests for isAllowingFor covering STOP
  signal, forward direction, reverse direction, after cancel, null segments,
  and second orientation.

- DefaultNetworkPerceptionPortTest: updated semaphore() helper to mock
  direction() = Cell.Segment.A and isAllowingFor(any(), any()), updated
  6 SemaphoreReading assertions to include authorizedFrom/authorizedTo.
@bedaHovorka
bedaHovorka force-pushed the copilot/fix-dynamic-rail-semaphore-display branch from 9c81093 to 50330d7 Compare July 28, 2026 16:05

@bedaHovorka bedaHovorka left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

@bedaHovorka
bedaHovorka marked this pull request as ready for review July 28, 2026 18:12
…play

Address PR #813 review findings (C1 + I1/I2/I3). The PR's isAllowingFor guard
was a no-op: it queried the static orientation, which always matched the
static forward, so separatorAspect/captureSignalState were unchanged and
toReading always reported the static forward direction — a lit semaphore
cleared for the reverse direction still read "proceed" to a forward-facing
observer. A static-orientation query cannot distinguish reserved-forward
from reserved-reverse; the semaphore must store the actual direction.

DynamicRailSemaphore now records the reservation direction in setUpSpeed
(cleared on STOP). authorizedDirection() returns the stored pair, defaulting
to the canonical forward when unknown (direct signal writes that bypass
setUpSpeed — entry/facing signals cleared outside the reservation flow).
isAllowingFor compares against the stored direction, so a forward query
against a reverse-reserved proceed aspect correctly returns false.

- DynamicRailSemaphore: authorizedFrom/authorizedTo + authorizedDirection();
  isAllowingFor rewritten against the stored direction.
- PerceptionMapping.separatorAspect: revert to raw signal. This is the train's
  own next-separator perception (the holder is always authorized), so a guard
  would wrongly show STOP to a reverse-travelling holder at its own semaphore.
- DefaultNetworkPerceptionPort.toReading: report authorizedDirection() so the
  LLM dispatcher's all_signal_aspects learns the actual reserved direction.
- SimulationCellRenderer: direction-aware canvas color — the forward-facing
  view shows STOP (RED) for a reverse-reserved aspect. AnimationStateCapture
  KDoc updated (its guard is now meaningful).
- Tests: reverse-reservation isAllowingFor/authorizedDirection coverage,
  new PerceptionMappingTest (commonTest), tightened DefaultNetworkPerceptionPortTest
  stub to drive toReading via authorizedDirection.

Co-Authored-By: Claude <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@bedaHovorka
bedaHovorka merged commit 665fcae into goal-10 Jul 28, 2026
3 checks passed
@bedaHovorka
bedaHovorka deleted the copilot/fix-dynamic-rail-semaphore-display branch July 28, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DynamicRailSemaphore signal display is direction-agnostic (canvas + all_signal_aspects show false proceed for the opposite direction)

2 participants