/*************************************************************************** file : traffic5.cpp created : September 2026 copyright : (C) 2026 by Bernhard Wymann email : berniw@bluewin.ch ***************************************************************************/ /* This program is free software; you can redistribute it and/or modify it * under the GNU General Public License, version 2 or (at your option) any * later version. Distributed WITHOUT ANY WARRANTY, including MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See . */ #include #include #include #include "traffic5.h" // The completed checkpoint includes passing; disable it only for the following exercise. static constexpr bool ENABLE_PASSING = true; static constexpr double MIN_GAP = 5.0; // metres between footprints static constexpr double FOLLOW_TIME = 1.0; // seconds static constexpr double SIDE_GAP = 1.2; // metres between footprints static constexpr double ROAD_MARGIN = 1.2; // metres outside our footprint static constexpr double OFFSET_RATE = 0.8; // metres per second static constexpr double PREDICTION_TIME = 3.0; // seconds, constant-speed estimate static constexpr double FOLLOW_DECELERATION = 3.0; // m/s^2, allowance for an uncertain leader static constexpr double LATERAL_RESPONSE = 0.6; // seconds of drift allowance / damping static constexpr double HOLD_TIME = 1.0; // seconds before reconsidering a cancelled manoeuvre static constexpr double CLEAR_REJOIN_SPEED = 2.0; // m/s, finish centering after space becomes available double wrappedDistance(double distance, double length) { return length > 0.0 ? std::remainder(distance, length) : distance; } TrafficCar describeCar(tCarElt* car) { TrafficCar result; result.car = car; const double tangent = RtTrackSideTgAngleL(&car->_trkPos); const double angle = car->_yaw - tangent; const double c = std::fabs(std::cos(angle)); const double s = std::fabs(std::sin(angle)); result.along = car->_trkPos.seg->lgfromstart + segmentProgress(car->_trkPos); result.lateral = car->_trkPos.toMiddle; result.speed = car->_speed_X * std::cos(tangent) + car->_speed_Y * std::sin(tangent); result.lateralSpeed = -car->_speed_X * std::sin(tangent) + car->_speed_Y * std::cos(tangent); result.halfLength = 0.5 * (car->_dimension_x * c + car->_dimension_y * s); result.halfWidth = 0.5 * (car->_dimension_x * s + car->_dimension_y * c); return result; } double approach(double value, double target, double maximumChange) { const double change = std::max(0.0, maximumChange); return value + std::max(-change, std::min(change, target - value)); } bool offsetTarget(tCarElt* car, double offset, int count, double length, Vector2& target) { const tTrackSeg* segment = car->_trkPos.seg; double along = segmentProgress(car->_trkPos) + std::min(8.0 + 0.33 * std::max(0.0, double(car->_speed_x)), length * 0.5); for (int visited = 0; visited <= count && segment; ++visited) { if (segment->length > 0.0 && along <= segment->length) { double tangent = segment->angle[TR_ZS]; if (segment->type != TR_STR && segment->radius > 0.0) { tangent += (segment->type == TR_LFT ? 1.0 : -1.0) * along / segment->radius; } target = centerlinePoint(segment, along) + Vector2{-std::sin(tangent), std::cos(tangent)} * offset; return true; } along -= std::max(0.0, double(segment->length)); segment = segment->next; } return false; } void applySpeedTarget(SpeedDriver& driver, tCarElt* car, double target, double brakeLimit) { if (car->_speed_x < -0.3) { return; // Chapter 4 already brakes before requesting a forward gear. } driver.applySpeedTarget(car, std::min(driver.targetSpeed, target), brakeLimit); } void TrafficPlanner::reset(double time) { mode = Mode::Cruising; opponents.clear(); passingCar = nullptr; offset = passOffset = 0.0; holdOffset = holdUntil = 0.0; previousTime = time; speedLimit = 0.0; nearby = emergency = false; corridorAvailable = true; } void TrafficPlanner::observe(tCarElt* car, tSituation* situation, double trackLength) { self = describeCar(car); opponents.clear(); nearby = false; for (int i = 0; i < situation->_ncars; ++i) { tCarElt* other = situation->cars[i]; if (other == car || (other->_state & RM_CAR_STATE_NO_SIMU)) { continue; } TrafficCar observation = describeCar(other); observation.along = wrappedDistance(observation.along - self.along, trackLength); // Keep the whole field: fixed distance windows can hide a stopped leader // at racing speed or a fast rear car during the six-second pit merge. opponents.push_back(observation); if (std::fabs(observation.along) < 18.0 && std::fabs(observation.lateral - self.lateral) < self.halfWidth + observation.halfWidth + 3.0) { nearby = true; } } } bool TrafficPlanner::corridorFree(double target, const tCarElt* except, double seconds) const { const double drift = self.lateral + LATERAL_RESPONSE * self.lateralSpeed; const double low = std::min({self.lateral, target, drift}) - self.halfWidth - SIDE_GAP; const double high = std::max({self.lateral, target, drift}) + self.halfWidth + SIDE_GAP; for (const TrafficCar& other : opponents) { if (other.car == except) { continue; } const double future = other.along + (other.speed - self.speed) * seconds; const double extent = self.halfLength + other.halfLength + MIN_GAP; const bool longitudinal = std::min(other.along, future) < extent && std::max(other.along, future) > -extent; if (longitudinal && other.lateral + other.halfWidth > low && other.lateral - other.halfWidth < high) { return false; } } return true; } void TrafficPlanner::lateralBounds(double roadHalfWidth, double& low, double& high) const { const double limit = std::max(0.0, roadHalfWidth - self.halfWidth - ROAD_MARGIN); low = -limit; high = limit; for (const TrafficCar& other : opponents) { if (std::fabs(other.along) < self.halfLength + other.halfLength + 4.0) { const double separation = self.halfWidth + other.halfWidth + SIDE_GAP; if (other.lateral >= self.lateral) { high = std::min(high, other.lateral - separation); } else { low = std::max(low, other.lateral + separation); } } } } void TrafficPlanner::plan(double time, double freeSpeed, double roadHalfWidth, double currentHalfWidth, double narrowingDistance) { const double elapsed = std::max(0.0, std::min(0.1, time - previousTime)); previousTime = time; speedLimit = freeSpeed; emergency = false; corridorAvailable = true; if (currentHalfWidth < 0.0) { currentHalfWidth = roadHalfWidth; } const double roadLimit = std::max(0.0, roadHalfWidth - self.halfWidth - ROAD_MARGIN); double low, high; lateralBounds(roadHalfWidth, low, high); double currentLow, currentHigh; lateralBounds(currentHalfWidth, currentLow, currentHigh); const TrafficCar* leader = nullptr; const TrafficCar* focus = nullptr; for (const TrafficCar& other : opponents) { if (other.car == passingCar) { focus = &other; } const bool overlap = std::fabs(other.lateral - self.lateral) < self.halfWidth + other.halfWidth + SIDE_GAP; if (other.along > 0.0 && overlap && (!leader || other.along < leader->along)) { leader = &other; } } if (mode == Mode::Passing && (!focus || focus->along + focus->halfLength + self.halfLength < -8.0)) { mode = Mode::Returning; passingCar = nullptr; } if (mode != Mode::Passing && mode != Mode::Returning && mode != Mode::Holding) { mode = leader ? Mode::Following : Mode::Cruising; if (ENABLE_PASSING && leader && freeSpeed - leader->speed > 1.5 && leader->along > 18.0) { const double separation = self.halfWidth + leader->halfWidth + SIDE_GAP + 0.5; const double passTime = std::max(PREDICTION_TIME, separation / OFFSET_RATE + 1.0); const double passDistance = std::max(70.0, std::max(0.0, self.speed - leader->speed) * passTime + MIN_GAP); const double candidates[] = {leader->lateral + separation, leader->lateral - separation}; for (double candidate : candidates) { if (leader->along < passDistance && candidate >= low && candidate <= high && corridorFree(candidate, leader->car, passTime)) { passOffset = candidate; passingCar = leader->car; mode = Mode::Passing; break; } } } } if (mode == Mode::Holding && time >= holdUntil && std::fabs(self.lateralSpeed) < 0.3 && low <= high) { const double returning = std::max(low, std::min(high, 0.0)); if (corridorFree(returning, nullptr, PREDICTION_TIME)) { mode = Mode::Returning; } } double wanted = mode == Mode::Passing ? passOffset : 0.0; if (mode == Mode::Passing && focus && focus->along > 18.0 && (focus->speed > freeSpeed + 2.0 || std::fabs(passOffset) > roadLimit)) { // The opportunity has gone. Return only if the swept lateral corridor is free. if (corridorFree(0.0, nullptr, PREDICTION_TIME)) { mode = Mode::Returning; passingCar = nullptr; wanted = 0.0; } } if (mode != Mode::Holding) { wanted = std::max(low, std::min(high, wanted)); const double next = approach(offset, wanted, OFFSET_RATE * elapsed); const tCarElt* except = mode == Mode::Passing ? passingCar : nullptr; // The leader exemption is valid only if the constrained passing lane still clears it. bool clearsLeader = true; for (const TrafficCar& other : opponents) { if (other.car == except && std::fabs(wanted - other.lateral) < self.halfWidth + other.halfWidth + SIDE_GAP) { clearsLeader = false; } } // Validate after ALL constraints, including the actual rate-limited next target. // Staying in the same lane behind a leader is handled by following-speed control. const bool movingSideways = std::fabs(wanted - self.lateral) > 0.15 || std::fabs(next - self.lateral) > 0.15 || std::fabs(self.lateralSpeed) > 0.3; if (low <= high && currentLow <= currentHigh && next >= currentLow && next <= currentHigh && clearsLeader && (!movingSideways || (corridorFree(wanted, except, PREDICTION_TIME) && corridorFree(next, except, PREDICTION_TIME)))) { offset = next; } else { mode = Mode::Holding; passingCar = nullptr; holdOffset = self.lateral; holdUntil = time + HOLD_TIME; } } if (mode == Mode::Holding) { // Withdraw the stale target immediately. Freezing it would keep steering toward it. // Anchor the hold to the entry position; do not chase the car's continuing drift. const double currentLimit = std::max(0.0, currentHalfWidth - self.halfWidth - ROAD_MARGIN); const double damped = holdOffset - LATERAL_RESPONSE * self.lateralSpeed; double hold = std::max(currentLow, std::min(currentHigh, damped)); corridorAvailable = currentLow <= currentHigh && corridorFree(hold, nullptr, PREDICTION_TIME); if (!corridorAvailable) { // No checked lateral alternative: oppose drift in our present lane, not in the old target lane. hold = std::max(-currentLimit, std::min(currentLimit, self.lateral - LATERAL_RESPONSE * self.lateralSpeed)); // An occupied starting corridor can reject even a move away from an // adjacent car. Never let the fallback steer back into that car's bounds. if (currentLow <= currentHigh) { hold = std::max(currentLow, std::min(currentHigh, hold)); } corridorAvailable = currentLow <= currentHigh && hold >= currentLow && hold <= currentHigh && corridorFree(hold, nullptr, PREDICTION_TIME); } offset = hold; } // Keep this bound while returning, too: a safe destination is not an instant lane change. // The intermediate target may use the present road width; slow before the narrower boundary. if ((currentHalfWidth > roadHalfWidth && std::fabs(self.lateral) > roadLimit) || low > high) { const double distance = currentHalfWidth > roadHalfWidth ? narrowingDistance : 0.0; const double available = std::max(0.0, distance - MIN_GAP - 0.5 * std::max(0.0, self.speed)); speedLimit = std::min(speedLimit, std::sqrt(2.0 * FOLLOW_DECELERATION * available)); if (available == 0.0) { // A cleared return must not get stranded merely outside the preferred margin. // Creep only if the actual projected body already fits the upcoming road. if (mode == Mode::Returning && corridorAvailable && std::fabs(self.lateral) + self.halfWidth + 0.3 < roadHalfWidth) { speedLimit = std::min(freeSpeed, CLEAR_REJOIN_SPEED); } else { emergency = true; } } } if (mode == Mode::Returning && std::fabs(offset) < 0.05 && std::fabs(self.lateral) < 0.5) { mode = leader ? Mode::Following : Mode::Cruising; } // Check cars ahead throughout the current-to-target corridor, including residual drift. // A rear approaching car alone does not request braking in front of it. const double drift = self.lateral + LATERAL_RESPONSE * self.lateralSpeed; const double corridorLow = std::min({self.lateral, offset, drift}) - self.halfWidth - SIDE_GAP; const double corridorHigh = std::max({self.lateral, offset, drift}) + self.halfWidth + SIDE_GAP; for (const TrafficCar& other : opponents) { if (other.along <= 0.0 || other.lateral + other.halfWidth <= corridorLow || other.lateral - other.halfWidth >= corridorHigh) { continue; } const double gap = other.along - self.halfLength - other.halfLength; const double desired = MIN_GAP + FOLLOW_TIME * std::max(0.0, self.speed); const double following = std::max(0.0, other.speed + 0.5 * (gap - desired)); const double stoppedBound = std::sqrt(2.0 * FOLLOW_DECELERATION * std::max(0.0, gap - MIN_GAP)); // Matching a moving leader uses its speed; a stopped leader uses the stop bound. const double brakingBound = std::sqrt(std::max(0.0, other.speed) * std::max(0.0, other.speed) + stoppedBound * stoppedBound); speedLimit = std::min(speedLimit, std::min(following, brakingBound)); if (gap < MIN_GAP) { speedLimit = 0.0; emergency = true; } } } void TrafficPlanner::drive(SpeedDriver& driver, tCarElt* car, tSituation* situation) { driver.drive(car, situation); // Reserve space over at least six seconds of travel, not a low-speed-only window. double halfWidth = car->_trkPos.seg->width / 2.0; const double roadHorizon = std::min(driver.trackLength, std::max(80.0, 6.0 * std::max(0.0, double(car->_speed_x)))); double narrowingDistance = roadHorizon; double distance = car->_trkPos.seg->length - segmentProgress(car->_trkPos); const tTrackSeg* segment = car->_trkPos.seg->next; for (int i = 1; i < driver.segmentCount && segment && distance < roadHorizon; ++i, segment = segment->next) { if (segment->width / 2.0 < halfWidth) { narrowingDistance = std::min(narrowingDistance, distance); halfWidth = segment->width / 2.0; } distance += std::max(0.0, double(segment->length)); } plan(situation->currentTime, driver.targetSpeed, halfWidth, car->_trkPos.seg->width / 2.0, narrowingDistance); if (car->_speed_x < -0.3) { return; } Vector2 target; if (offsetTarget(car, offset, driver.segmentCount, driver.trackLength, target)) { car->ctrl.steer = static_cast(steerToward(car, target)); } if (speedLimit < driver.targetSpeed) { applySpeedTarget(driver, car, speedLimit, 1.0); } if (emergency) { car->ctrl.accelCmd = 0.0f; car->ctrl.brakeCmd = static_cast(driver.filterABS(car, 1.0)); } }