diff --git a/ezgui/src/tools/wizard.rs b/ezgui/src/tools/wizard.rs index 1750941772..bf294f8bdb 100644 --- a/ezgui/src/tools/wizard.rs +++ b/ezgui/src/tools/wizard.rs @@ -190,18 +190,14 @@ impl<'a, 'b> WrappedWizard<'a, 'b> { ) } - pub fn input_percent(&mut self, query: &str) -> Option { + pub fn input_percent(&mut self, query: &str) -> Option { self.input_something( query, None, Box::new(|line| { - line.parse::().ok().and_then(|num| { - if num >= 0.0 && num <= 1.0 { - Some(num) - } else { - None - } - }) + line.parse::() + .ok() + .and_then(|num| if num <= 100 { Some(num) } else { None }) }), ) } diff --git a/game/src/sandbox/dashboards/trip_table.rs b/game/src/sandbox/dashboards/trip_table.rs index ba66263c71..71a7e098f9 100644 --- a/game/src/sandbox/dashboards/trip_table.rs +++ b/game/src/sandbox/dashboards/trip_table.rs @@ -333,14 +333,22 @@ fn make(ctx: &mut EventCtx, app: &App, opts: &Options) -> Composite { ]) .margin_below(5), ); + let (_, unfinished, _) = app.primary.sim.num_trips(); col.push( - format!( - "{} trips aborted due to simulation glitch", - prettyprint_usize(aborted) - ) - .draw_text(ctx) - .margin_below(5), + Text::from_multiline(vec![ + Line(format!( + "{} trips aborted due to simulation glitch", + prettyprint_usize(aborted) + )), + Line(format!( + "{} unfinished trips remaining", + prettyprint_usize(unfinished) + )), + ]) + .draw(ctx) + .margin_below(10), ); + col.push( Widget::row(vec![ if opts.skip > 0 { diff --git a/game/src/sandbox/gameplay/freeform.rs b/game/src/sandbox/gameplay/freeform.rs index 3f7d448458..fe5762351e 100644 --- a/game/src/sandbox/gameplay/freeform.rs +++ b/game/src/sandbox/gameplay/freeform.rs @@ -263,6 +263,7 @@ impl State for AgentSpawner { self.composite.dropdown_value("mode"), map, ), + cancelled: false, }], }); } @@ -529,6 +530,7 @@ pub fn spawn_agents_around(i: IntersectionID, app: &mut App) { origin: None, }, TripEndpoint::Border(lane.src_i, None), + false, map, ); } @@ -549,6 +551,7 @@ pub fn spawn_agents_around(i: IntersectionID, app: &mut App) { ), }, TripEndpoint::Border(lane.src_i, None), + false, map, ); } diff --git a/game/src/sandbox/gameplay/mod.rs b/game/src/sandbox/gameplay/mod.rs index 00779ebc09..d0f3c12832 100644 --- a/game/src/sandbox/gameplay/mod.rs +++ b/game/src/sandbox/gameplay/mod.rs @@ -126,7 +126,7 @@ impl GameplayMode { }; if let GameplayMode::PlayScenario(_, _, ref modifiers) = self { for m in modifiers { - scenario = m.apply(scenario); + scenario = m.apply(scenario, &mut rng); } } scenario diff --git a/game/src/sandbox/gameplay/play_scenario.rs b/game/src/sandbox/gameplay/play_scenario.rs index 60fe99a468..2233f7f91e 100644 --- a/game/src/sandbox/gameplay/play_scenario.rs +++ b/game/src/sandbox/gameplay/play_scenario.rs @@ -258,19 +258,27 @@ impl State for EditScenarioModifiers { fn new_modifier(scenario_name: String, modifiers: Vec) -> Box { WizardState::new(Box::new(move |wiz, ctx, app| { let mut wizard = wiz.wrap(ctx); - match wizard.choose_string("", || vec!["repeat days"])?.as_str() { - x if x == "repeat days" => { - let n = wizard.input_usize("Repeat everyone's schedule how many days?")?; - let mut mods = modifiers.clone(); - mods.push(ScenarioModifier::RepeatDays(n)); - Some(Transition::PopThenReplace(EditScenarioModifiers::new( - ctx, - app, - scenario_name.clone(), - mods, - ))) - } + let new_mod = match wizard + .choose_string("", || { + vec!["repeat days", "cancel all trips for some people"] + })? + .as_str() + { + x if x == "repeat days" => ScenarioModifier::RepeatDays( + wizard.input_usize("Repeat everyone's schedule how many days?")?, + ), + x if x == "cancel all trips for some people" => ScenarioModifier::CancelPeople( + wizard.input_percent("What percent of people should cancel trips? (0 to 100)")?, + ), _ => unreachable!(), - } + }; + let mut mods = modifiers.clone(); + mods.push(new_mod); + Some(Transition::PopThenReplace(EditScenarioModifiers::new( + ctx, + app, + scenario_name.clone(), + mods, + ))) })) } diff --git a/game/src/sandbox/gameplay/tutorial.rs b/game/src/sandbox/gameplay/tutorial.rs index acaae74968..274ecb1ab3 100644 --- a/game/src/sandbox/gameplay/tutorial.rs +++ b/game/src/sandbox/gameplay/tutorial.rs @@ -1097,6 +1097,7 @@ impl TutorialState { goal: DrivingGoal::ParkNear(goal_bldg), is_bike: false, }, + cancelled: false, }], }); // Will definitely get there first @@ -1114,6 +1115,7 @@ impl TutorialState { goal: DrivingGoal::ParkNear(goal_bldg), is_bike: false, }, + cancelled: false, }], }); } diff --git a/game/src/sandbox/speed.rs b/game/src/sandbox/speed.rs index e29c24a36f..9249142c4d 100644 --- a/game/src/sandbox/speed.rs +++ b/game/src/sandbox/speed.rs @@ -741,8 +741,8 @@ fn compare_count(after: usize, before: usize) -> String { if after == before { "+0".to_string() } else if after > before { - format!("+{}", after - before) + format!("+{}", prettyprint_usize(after - before)) } else { - format!("-{}", before - after) + format!("-{}", prettyprint_usize(before - after)) } } diff --git a/importer/src/soundcast/trips.rs b/importer/src/soundcast/trips.rs index 816878989e..b93c8831ed 100644 --- a/importer/src/soundcast/trips.rs +++ b/importer/src/soundcast/trips.rs @@ -258,7 +258,11 @@ pub fn make_weekday_scenario( }) { let idx = individ_trips.len(); - individ_trips.push(Some(IndividTrip { depart, trip })); + individ_trips.push(Some(IndividTrip { + depart, + trip, + cancelled: false, + })); trips_per_person.insert(person, (seq, idx)); } timer.note(format!( @@ -328,6 +332,7 @@ pub fn make_weekday_scenario_with_everyone( individ_trips.push(Some(IndividTrip { depart: orig_trip.depart_at, trip, + cancelled: false, })); trips_per_person.insert(orig_trip.person, (orig_trip.seq, idx)); } diff --git a/sim/src/make/generator.rs b/sim/src/make/generator.rs index 1ede3967b3..2ac2358d07 100644 --- a/sim/src/make/generator.rs +++ b/sim/src/make/generator.rs @@ -185,6 +185,7 @@ impl SpawnOverTime { trips: vec![IndividTrip { depart, trip: SpawnTrip::UsingParkedCar(from_bldg, goal), + cancelled: false, }], }); return; @@ -204,6 +205,7 @@ impl SpawnOverTime { trips: vec![IndividTrip { depart, trip: SpawnTrip::UsingBike(start_spot, goal), + cancelled: false, }], }); return; @@ -228,6 +230,7 @@ impl SpawnOverTime { trips: vec![IndividTrip { depart, trip: SpawnTrip::UsingTransit(start_spot, goal, route, stop1, stop2), + cancelled: false, }], }); return; @@ -240,6 +243,7 @@ impl SpawnOverTime { trips: vec![IndividTrip { depart, trip: SpawnTrip::JustWalking(start_spot, goal), + cancelled: false, }], }); return; @@ -295,6 +299,7 @@ impl BorderSpawnOverTime { stop1, stop2, ), + cancelled: false, }], }); continue; @@ -307,6 +312,7 @@ impl BorderSpawnOverTime { trips: vec![IndividTrip { depart, trip: SpawnTrip::JustWalking(start.clone(), goal), + cancelled: false, }], }); } @@ -337,6 +343,7 @@ impl BorderSpawnOverTime { is_bike: constraints == PathConstraints::Bike, origin: None, }, + cancelled: false, }], }); } diff --git a/sim/src/make/modifier.rs b/sim/src/make/modifier.rs index 324394914b..0cf9ebe3ab 100644 --- a/sim/src/make/modifier.rs +++ b/sim/src/make/modifier.rs @@ -1,23 +1,30 @@ use crate::{IndividTrip, Scenario}; use geom::Duration; +use rand::Rng; +use rand_xorshift::XorShiftRng; #[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] pub enum ScenarioModifier { RepeatDays(usize), + CancelPeople(usize), } impl ScenarioModifier { - pub fn apply(&self, s: Scenario) -> Scenario { - let mut s = match self { + // If this modifies scenario_name, then that means prebaked results don't match up and + // shouldn't be used. + pub fn apply(&self, s: Scenario, rng: &mut XorShiftRng) -> Scenario { + match self { ScenarioModifier::RepeatDays(n) => repeat_days(s, *n), - }; - s.scenario_name = format!("{} (modified)", s.scenario_name); - s + ScenarioModifier::CancelPeople(pct) => cancel_people(s, *pct, rng), + } } pub fn describe(&self) -> String { match self { ScenarioModifier::RepeatDays(n) => format!("repeat the entire day {} times", n), + ScenarioModifier::CancelPeople(pct) => { + format!("cancel all trips for {}% of people", pct) + } } } } @@ -31,6 +38,7 @@ impl ScenarioModifier { // The bigger problem is that any people that seem to require multiple cars... will wind up // needing LOTS of cars. fn repeat_days(mut s: Scenario, days: usize) -> Scenario { + s.scenario_name = format!("{} (repeated {} days)", s.scenario_name, days); for person in &mut s.people { let mut trips = Vec::new(); let mut offset = Duration::ZERO; @@ -39,6 +47,7 @@ fn repeat_days(mut s: Scenario, days: usize) -> Scenario { trips.push(IndividTrip { depart: trip.depart + offset, trip: trip.trip.clone(), + cancelled: false, }); } offset += Duration::hours(24); @@ -47,3 +56,17 @@ fn repeat_days(mut s: Scenario, days: usize) -> Scenario { } s } + +fn cancel_people(mut s: Scenario, pct: usize, rng: &mut XorShiftRng) -> Scenario { + let pct = (pct as f64) / 100.0; + for person in &mut s.people { + if rng.gen_bool(pct) { + // TODO It's not obvious how to cancel individual trips. How are later trips affected? + // What if a car doesn't get moved to another place? + for trip in &mut person.trips { + trip.cancelled = true; + } + } + } + s +} diff --git a/sim/src/make/scenario.rs b/sim/src/make/scenario.rs index aa61f40f36..b4f67dece4 100644 --- a/sim/src/make/scenario.rs +++ b/sim/src/make/scenario.rs @@ -37,6 +37,7 @@ pub struct PersonSpec { pub struct IndividTrip { pub depart: Time, pub trip: SpawnTrip, + pub cancelled: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -124,7 +125,7 @@ impl Scenario { &mut tmp_rng, map, ); - spawner.schedule_trip(person, t.depart, spec, t.trip.start(map), map); + spawner.schedule_trip(person, t.depart, spec, t.trip.start(map), t.cancelled, map); } } @@ -575,6 +576,7 @@ impl PersonSpec { // For each indexed car, is it parked somewhere, or off-map? let mut car_locations: Vec<(usize, Option)> = Vec::new(); + // TODO If the trip is cancelled, this should be affected... for trip in &self.trips { let use_for_trip = match trip.trip { SpawnTrip::VehicleAppearing { diff --git a/sim/src/make/spawner.rs b/sim/src/make/spawner.rs index 23c1badce4..7a77f8c902 100644 --- a/sim/src/make/spawner.rs +++ b/sim/src/make/spawner.rs @@ -62,7 +62,7 @@ pub enum TripSpec { // This structure is created temporarily by a Scenario or to interactively spawn agents. pub struct TripSpawner { - trips: Vec<(PersonID, Time, TripSpec, TripEndpoint)>, + trips: Vec<(PersonID, Time, TripSpec, TripEndpoint, bool)>, } impl TripSpawner { @@ -76,6 +76,7 @@ impl TripSpawner { start_time: Time, spec: TripSpec, trip_start: TripEndpoint, + cancelled: bool, map: &Map, ) { // TODO We'll want to repeat this validation when we spawn stuff later for a second leg... @@ -164,6 +165,7 @@ impl TripSpawner { goal: SidewalkSpot::building(*b, map), }, trip_start, + cancelled, )); return; } @@ -173,7 +175,8 @@ impl TripSpawner { TripSpec::Remote { .. } => {} }; - self.trips.push((person.id, start_time, spec, trip_start)); + self.trips + .push((person.id, start_time, spec, trip_start, cancelled)); } pub fn finalize( @@ -209,7 +212,7 @@ impl TripSpawner { } timer.start_iter("spawn trips", paths.len()); - for ((p, start_time, spec, trip_start), maybe_req, maybe_path) in paths { + for ((p, start_time, spec, trip_start, cancelled), maybe_req, maybe_path) in paths { timer.next(); // TODO clone() is super weird to do here, but we just need to make the borrow checker @@ -331,10 +334,15 @@ impl TripSpawner { map, ), }; - scheduler.push( - start_time, - Command::StartTrip(trip, spec, maybe_req, maybe_path), - ); + + if cancelled { + trips.cancel_trip(trip); + } else { + scheduler.push( + start_time, + Command::StartTrip(trip, spec, maybe_req, maybe_path), + ); + } } } } diff --git a/sim/src/trips.rs b/sim/src/trips.rs index 0c7a2bb0f5..887b01716f 100644 --- a/sim/src/trips.rs +++ b/sim/src/trips.rs @@ -668,6 +668,14 @@ impl TripManager { self.person_finished_trip(now, person, parking, scheduler, map); } + // Different than aborting a trip. Don't warp any vehicles or change where the person is. + pub fn cancel_trip(&mut self, id: TripID) { + let trip = &mut self.trips[id.0]; + self.unfinished_trips -= 1; + trip.aborted = true; + self.events.push(Event::TripAborted(trip.id)); + } + pub fn abort_trip( &mut self, now: Time, @@ -918,6 +926,7 @@ impl TripManager { scheduler: &mut Scheduler, map: &Map, ) { + assert!(!self.trips[trip.0].aborted); if !self.pathfinding_upfront && maybe_path.is_none() && maybe_req.is_some() { maybe_path = map.pathfind(maybe_req.clone().unwrap()); }