Goto controller algorithm #20

Closed
opened 2026-08-17 16:58:09 +02:00 by SirStone · 2 comments
Owner

Parent map: #18

Question

What algorithm should the goto controller use to translate a target (x, y) into per-tick setTargetSpeed and setTurnRate commands?

Considerations:

  • Must choose forward vs reverse based on which reaches target faster (user requirement)
  • Tank Royale physics: turn rate depends on speed (max 10°/tick at speed 0, down to 4°/tick at speed 8)
  • Should it turn-then-drive, or arc toward the target (proportional navigation)?
  • How to handle arrival (deceleration near target)?
  • Tank Royale's setDistanceRemaining API — does it exist and can it be leveraged?
Parent map: #18 ## Question What algorithm should the goto controller use to translate a target (x, y) into per-tick `setTargetSpeed` and `setTurnRate` commands? Considerations: - Must choose forward vs reverse based on which reaches target faster (user requirement) - Tank Royale physics: turn rate depends on speed (max 10°/tick at speed 0, down to 4°/tick at speed 8) - Should it turn-then-drive, or arc toward the target (proportional navigation)? - How to handle arrival (deceleration near target)? - Tank Royale's `setDistanceRemaining` API — does it exist and can it be leveraged?
SirStone added the wayfinder:research label 2026-08-17 16:58:09 +02:00
Author
Owner

Research complete

Full writeup: docs/research/goto-controller-algorithm.md on branch research/goto-controller.


Recommendation: forward/reverse proportional steering with getNewTargetSpeed

Per-tick pseudocode:

proc gotoTick*(tx, ty, x, y, direction, currentSpeed: float):
    tuple[targetSpeed, turnRate: float] =

  let dist = distanceTo(x, y, tx, ty)
  if dist < ARRIVAL_THRESHOLD:   # 18.0 = BOT_RADIUS
    return (0.0, 0.0)

  let rawBearing = normalizeRelativeAngle(directionTo(x, y, tx, ty) - direction)

  # Forward vs reverse: reversing beats turning >90°
  let (dirSign, effBearing) =
    if abs(rawBearing) > 90.0:
      (-1.0, normalizeRelativeAngle(rawBearing + 180.0))
    else:
      (1.0, rawBearing)

  let maxTurn   = 10.0 - 0.75 * abs(currentSpeed)   # calcMaxTurnRate
  let turnRate  = effBearing.clamp(-maxTurn, maxTurn)

  # Reuse existing utils.nim function — encodes exact asymmetric accel/decel
  let rawSpeed    = getNewTargetSpeed(MAX_SPEED, abs(currentSpeed), dist)
  let targetSpeed = dirSign * rawSpeed

  return (targetSpeed, turnRate)

Key decisions

Decision Rationale
Forward vs reverse at 90° threshold Exact breakeven: turning >90° forward always costs more ticks than reversing the complementary angle
Turn rate = effBearing.clamp(-maxTurn, maxTurn) Proportional for small errors (smooth), saturates for large errors (fastest turn) — same pattern already in actions.nim
Speed via getNewTargetSpeed Already in utils.nim; encodes accel +1 / decel −2 asymmetry correctly so the bot brakes exactly on target with no overshoot
No separate deceleration logic Server handles it via the same function — the controller just sets target speed each tick

What was NOT chosen

  • Turn-then-drive: sequential, jerky, wastes ticks.
  • Pure proportional nav without speed clamping: works emergently but wastes the speed-dependent turn headroom — the explicit clamp is more correct and already used in actions.nim.
## Research complete Full writeup: [`docs/research/goto-controller-algorithm.md`](../../blob/research/goto-controller/docs/research/goto-controller-algorithm.md) on branch `research/goto-controller`. --- ### Recommendation: forward/reverse proportional steering with `getNewTargetSpeed` **Per-tick pseudocode:** ```nim proc gotoTick*(tx, ty, x, y, direction, currentSpeed: float): tuple[targetSpeed, turnRate: float] = let dist = distanceTo(x, y, tx, ty) if dist < ARRIVAL_THRESHOLD: # 18.0 = BOT_RADIUS return (0.0, 0.0) let rawBearing = normalizeRelativeAngle(directionTo(x, y, tx, ty) - direction) # Forward vs reverse: reversing beats turning >90° let (dirSign, effBearing) = if abs(rawBearing) > 90.0: (-1.0, normalizeRelativeAngle(rawBearing + 180.0)) else: (1.0, rawBearing) let maxTurn = 10.0 - 0.75 * abs(currentSpeed) # calcMaxTurnRate let turnRate = effBearing.clamp(-maxTurn, maxTurn) # Reuse existing utils.nim function — encodes exact asymmetric accel/decel let rawSpeed = getNewTargetSpeed(MAX_SPEED, abs(currentSpeed), dist) let targetSpeed = dirSign * rawSpeed return (targetSpeed, turnRate) ``` ### Key decisions | Decision | Rationale | |---|---| | **Forward vs reverse at 90° threshold** | Exact breakeven: turning >90° forward always costs more ticks than reversing the complementary angle | | **Turn rate = `effBearing.clamp(-maxTurn, maxTurn)`** | Proportional for small errors (smooth), saturates for large errors (fastest turn) — same pattern already in `actions.nim` | | **Speed via `getNewTargetSpeed`** | Already in `utils.nim`; encodes accel +1 / decel −2 asymmetry correctly so the bot brakes exactly on target with no overshoot | | **No separate deceleration logic** | Server handles it via the same function — the controller just sets target speed each tick | ### What was NOT chosen - **Turn-then-drive**: sequential, jerky, wastes ticks. - **Pure proportional nav without speed clamping**: works emergently but wastes the speed-dependent turn headroom — the explicit clamp is more correct and already used in `actions.nim`.
Author
Owner

Resolution

Proportional steering with forward/reverse selection, reusing existing getNewTargetSpeed from utils.nim.

Algorithm:

  1. Compute raw bearing from tank to target: arctan2(target - position) - heading
  2. If |bearing| > 90°: reverse (negate speed, adjust bearing by 180°)
  3. Turn rate = bearing clamped to speed-dependent max ±(10 - 0.75 × |speed|)
  4. Target speed = dirSign × getNewTargetSpeed(MAX_SPEED, |speed|, distance)

Key facts:

  • getNewTargetSpeed already in utils.nim — handles asymmetric accel(+1)/decel(-2) model
  • Fully non-blocking, pure function, one call per tick
  • Research written up at docs/research/goto-controller-algorithm.md on branch research/goto-controller
## Resolution **Proportional steering with forward/reverse selection**, reusing existing `getNewTargetSpeed` from `utils.nim`. Algorithm: 1. Compute raw bearing from tank to target: `arctan2(target - position) - heading` 2. If `|bearing| > 90°`: reverse (negate speed, adjust bearing by 180°) 3. Turn rate = bearing clamped to speed-dependent max `±(10 - 0.75 × |speed|)` 4. Target speed = `dirSign × getNewTargetSpeed(MAX_SPEED, |speed|, distance)` Key facts: - `getNewTargetSpeed` already in `utils.nim` — handles asymmetric accel(+1)/decel(-2) model - Fully non-blocking, pure function, one call per tick - Research written up at `docs/research/goto-controller-algorithm.md` on branch `research/goto-controller`
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: SirStone/SirRoboGarage#20