mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-25 11:44:25 +03:00
teasing apart some of the gameplay code
This commit is contained in:
parent
9b82e7288b
commit
5037b9e077
@ -1,661 +0,0 @@
|
||||
use crate::common::{Plot, Series};
|
||||
use crate::game::{msg, Transition, WizardState};
|
||||
use crate::helpers::rotating_color_total;
|
||||
use crate::render::AgentColorScheme;
|
||||
use crate::sandbox::overlays::Overlays;
|
||||
use crate::sandbox::{bus_explorer, spawner, SandboxMode};
|
||||
use crate::ui::UI;
|
||||
use abstutil::{prettyprint_usize, Timer};
|
||||
use ezgui::{
|
||||
hotkey, lctrl, Choice, Color, EventCtx, GfxCtx, Key, Line, ModalMenu, Text, TextSpan, Wizard,
|
||||
};
|
||||
use geom::{Duration, Statistic};
|
||||
use map_model::BusRouteID;
|
||||
use sim::{Analytics, Scenario, TripMode};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum GameplayMode {
|
||||
// TODO Maybe this should be "sandbox"
|
||||
Freeform,
|
||||
PlayScenario(String),
|
||||
// Route name
|
||||
OptimizeBus(String),
|
||||
CreateGridlock,
|
||||
// TODO Be able to filter population by more factors
|
||||
FasterTrips(TripMode),
|
||||
}
|
||||
|
||||
pub struct GameplayState {
|
||||
pub mode: GameplayMode,
|
||||
pub menu: ModalMenu,
|
||||
state: State,
|
||||
prebaked: Analytics,
|
||||
}
|
||||
|
||||
enum State {
|
||||
// TODO Maybe this one could remember what things were spawned, offer to replay this later
|
||||
Freeform,
|
||||
PlayScenario,
|
||||
OptimizeBus {
|
||||
route: BusRouteID,
|
||||
time: Duration,
|
||||
stat: Statistic,
|
||||
},
|
||||
CreateGridlock {
|
||||
time: Duration,
|
||||
},
|
||||
FasterTrips {
|
||||
mode: TripMode,
|
||||
time: Duration,
|
||||
},
|
||||
}
|
||||
|
||||
impl GameplayState {
|
||||
pub fn initialize(mode: GameplayMode, ui: &mut UI, ctx: &mut EventCtx) -> GameplayState {
|
||||
let prebaked: Analytics = abstutil::read_binary(
|
||||
&abstutil::path_prebaked_results(ui.primary.map.get_name()),
|
||||
&mut Timer::throwaway(),
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
println!("WARNING! No prebaked sim analytics. Only freeform mode will work.");
|
||||
Analytics::new()
|
||||
});
|
||||
|
||||
let (state, maybe_scenario) = match mode.clone() {
|
||||
GameplayMode::Freeform => (
|
||||
GameplayState {
|
||||
mode,
|
||||
menu: ModalMenu::new(
|
||||
"Freeform mode",
|
||||
vec![
|
||||
(hotkey(Key::S), "start a scenario"),
|
||||
(lctrl(Key::L), "load another map"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
)
|
||||
.disable_standalone_layout(),
|
||||
state: State::Freeform,
|
||||
prebaked,
|
||||
},
|
||||
None,
|
||||
),
|
||||
GameplayMode::PlayScenario(scenario) => (
|
||||
GameplayState {
|
||||
mode,
|
||||
menu: ModalMenu::new(
|
||||
&format!("Playing {}", scenario),
|
||||
vec![
|
||||
(hotkey(Key::S), "start another scenario"),
|
||||
(lctrl(Key::L), "load another map"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
)
|
||||
.disable_standalone_layout(),
|
||||
state: State::PlayScenario,
|
||||
prebaked,
|
||||
},
|
||||
Some(scenario),
|
||||
),
|
||||
GameplayMode::OptimizeBus(route_name) => {
|
||||
let route = ui.primary.map.get_bus_route(&route_name).unwrap();
|
||||
(
|
||||
GameplayState {
|
||||
mode,
|
||||
menu: ModalMenu::new(
|
||||
&format!("Optimize {}", route_name),
|
||||
vec![
|
||||
(hotkey(Key::E), "show bus route"),
|
||||
(hotkey(Key::T), "show delays over time"),
|
||||
(hotkey(Key::S), "change statistic"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
)
|
||||
.disable_standalone_layout(),
|
||||
state: State::OptimizeBus {
|
||||
route: route.id,
|
||||
time: Duration::ZERO,
|
||||
stat: Statistic::Max,
|
||||
},
|
||||
prebaked,
|
||||
},
|
||||
Some("weekday_typical_traffic_from_psrc".to_string()),
|
||||
)
|
||||
}
|
||||
GameplayMode::CreateGridlock => (
|
||||
GameplayState {
|
||||
mode,
|
||||
menu: ModalMenu::new(
|
||||
"Cause gridlock",
|
||||
vec![
|
||||
(hotkey(Key::E), "show agent delay"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
)
|
||||
.disable_standalone_layout(),
|
||||
state: State::CreateGridlock {
|
||||
time: Duration::ZERO,
|
||||
},
|
||||
prebaked,
|
||||
},
|
||||
Some("weekday_typical_traffic_from_psrc".to_string()),
|
||||
),
|
||||
GameplayMode::FasterTrips(trip_mode) => (
|
||||
GameplayState {
|
||||
mode,
|
||||
menu: ModalMenu::new(
|
||||
&format!("Speed up {} trips", trip_mode),
|
||||
vec![(hotkey(Key::H), "help")],
|
||||
ctx,
|
||||
)
|
||||
.disable_standalone_layout(),
|
||||
state: State::FasterTrips {
|
||||
mode: trip_mode,
|
||||
time: Duration::ZERO,
|
||||
},
|
||||
prebaked,
|
||||
},
|
||||
Some("weekday_typical_traffic_from_psrc".to_string()),
|
||||
),
|
||||
};
|
||||
if let Some(scenario_name) = maybe_scenario {
|
||||
ctx.loading_screen("instantiate scenario", |_, timer| {
|
||||
let num_agents = ui.primary.current_flags.num_agents;
|
||||
let builtin = if let Some(n) = num_agents {
|
||||
format!("random scenario with {} agents", n)
|
||||
} else {
|
||||
"random scenario with some agents".to_string()
|
||||
};
|
||||
let scenario = if scenario_name == builtin {
|
||||
if let Some(n) = num_agents {
|
||||
Scenario::scaled_run(&ui.primary.map, n)
|
||||
} else {
|
||||
Scenario::small_run(&ui.primary.map)
|
||||
}
|
||||
} else if scenario_name == "just buses" {
|
||||
let mut s = Scenario::empty(&ui.primary.map);
|
||||
s.scenario_name = "just buses".to_string();
|
||||
s.seed_buses = true;
|
||||
s
|
||||
} else {
|
||||
abstutil::read_binary(
|
||||
&abstutil::path1_bin(
|
||||
&ui.primary.map.get_name(),
|
||||
abstutil::SCENARIOS,
|
||||
&scenario_name,
|
||||
),
|
||||
timer,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
scenario.instantiate(
|
||||
&mut ui.primary.sim,
|
||||
&ui.primary.map,
|
||||
&mut ui.primary.current_flags.sim_flags.make_rng(),
|
||||
timer,
|
||||
);
|
||||
ui.primary.sim.step(&ui.primary.map, Duration::seconds(0.1));
|
||||
});
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
overlays: &mut Overlays,
|
||||
) -> Option<Transition> {
|
||||
match self.state {
|
||||
State::Freeform => {
|
||||
self.menu.event(ctx);
|
||||
if self.menu.action("start a scenario") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
change_scenario,
|
||||
))));
|
||||
}
|
||||
if self.menu.action("load another map") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(load_map))));
|
||||
}
|
||||
if self.menu.action("help") {
|
||||
return Some(Transition::Push(msg("Help", vec!["This simulation is empty by default.", "Try right-clicking an intersection and choosing to spawn agents (or just hover over it and press Z).", "You can also spawn agents from buildings or lanes.", "You can also start a full scenario to get realistic traffic."])));
|
||||
}
|
||||
if let Some(new_state) = spawner::AgentSpawner::new(ctx, ui) {
|
||||
return Some(Transition::Push(new_state));
|
||||
}
|
||||
if let Some(new_state) = spawner::SpawnManyAgents::new(ctx, ui) {
|
||||
return Some(Transition::Push(new_state));
|
||||
}
|
||||
}
|
||||
State::PlayScenario => {
|
||||
self.menu.event(ctx);
|
||||
if self.menu.action("start another scenario") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
change_scenario,
|
||||
))));
|
||||
}
|
||||
if self.menu.action("load another map") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(load_map))));
|
||||
}
|
||||
if self.menu.action("help") {
|
||||
return Some(Transition::Push(msg(
|
||||
"Help",
|
||||
vec![
|
||||
"Do things seem a bit quiet?",
|
||||
"The simulation starts at midnight, so you might need to wait a bit.",
|
||||
"Try using the speed controls on the left.",
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
State::OptimizeBus {
|
||||
route,
|
||||
ref mut time,
|
||||
ref mut stat,
|
||||
} => {
|
||||
self.menu.event(ctx);
|
||||
if manage_overlays(
|
||||
&mut self.menu,
|
||||
ctx,
|
||||
"show bus route",
|
||||
"hide bus route",
|
||||
overlays,
|
||||
match overlays {
|
||||
Overlays::BusRoute(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
*time != ui.primary.sim.time(),
|
||||
) {
|
||||
*overlays = Overlays::BusRoute(bus_explorer::ShowBusRoute::new(
|
||||
ui.primary.map.get_br(route),
|
||||
ui,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
if manage_overlays(
|
||||
&mut self.menu,
|
||||
ctx,
|
||||
"show delays over time",
|
||||
"hide delays over time",
|
||||
overlays,
|
||||
match overlays {
|
||||
Overlays::BusDelaysOverTime(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
*time != ui.primary.sim.time(),
|
||||
) {
|
||||
*overlays = Overlays::BusDelaysOverTime(bus_delays(route, ui, ctx));
|
||||
}
|
||||
|
||||
// TODO Expensive
|
||||
if *time != ui.primary.sim.time() {
|
||||
*time = ui.primary.sim.time();
|
||||
self.menu
|
||||
.set_info(ctx, bus_route_panel(route, ui, *stat, &self.prebaked));
|
||||
}
|
||||
|
||||
if self.menu.action("change statistic") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
move |wiz, ctx, _| {
|
||||
// TODO Filter out existing. Make this kind of thing much easier.
|
||||
let (_, new_stat) = wiz.wrap(ctx).choose(
|
||||
"Show which statistic on frequency a bus stop is visited?",
|
||||
|| {
|
||||
Statistic::all()
|
||||
.into_iter()
|
||||
.map(|s| Choice::new(s.to_string(), s))
|
||||
.collect()
|
||||
},
|
||||
)?;
|
||||
Some(Transition::PopWithData(Box::new(move |state, _, _| {
|
||||
let sandbox = state.downcast_mut::<SandboxMode>().unwrap();
|
||||
match sandbox.gameplay.state {
|
||||
State::OptimizeBus {
|
||||
ref mut stat,
|
||||
ref mut time,
|
||||
..
|
||||
} => {
|
||||
// Force recalculation
|
||||
*time = Duration::ZERO;
|
||||
*stat = new_stat;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
})))
|
||||
},
|
||||
))));
|
||||
}
|
||||
if self.menu.action("help") {
|
||||
return Some(Transition::Push(msg(
|
||||
"Help",
|
||||
vec![
|
||||
"First find where the bus gets stuck.",
|
||||
"Then use edit mode to try to speed things up.",
|
||||
"Try making dedicated bus lanes",
|
||||
"and adjusting traffic signals.",
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
||||
State::CreateGridlock { ref mut time } => {
|
||||
self.menu.event(ctx);
|
||||
manage_acs(
|
||||
&mut self.menu,
|
||||
ctx,
|
||||
ui,
|
||||
"show agent delay",
|
||||
"hide agent delay",
|
||||
AgentColorScheme::Delay,
|
||||
);
|
||||
|
||||
if *time != ui.primary.sim.time() {
|
||||
*time = ui.primary.sim.time();
|
||||
self.menu.set_info(ctx, gridlock_panel(ui, &self.prebaked));
|
||||
}
|
||||
|
||||
if self.menu.action("help") {
|
||||
return Some(Transition::Push(msg("Help", vec![
|
||||
"You might notice a few places in the map where gridlock forms already.",
|
||||
"You can make things worse!",
|
||||
"How few lanes can you close for construction before everything grinds to a halt?",
|
||||
])));
|
||||
}
|
||||
}
|
||||
State::FasterTrips { mode, ref mut time } => {
|
||||
self.menu.event(ctx);
|
||||
|
||||
if *time != ui.primary.sim.time() {
|
||||
*time = ui.primary.sim.time();
|
||||
self.menu
|
||||
.set_info(ctx, faster_trips_panel(mode, ui, &self.prebaked));
|
||||
}
|
||||
|
||||
if self.menu.action("help") {
|
||||
return Some(Transition::Push(msg(
|
||||
"Help",
|
||||
vec!["How can you possibly speed up all trips of some mode?"],
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
self.menu.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
fn bus_route_panel(id: BusRouteID, ui: &UI, stat: Statistic, prebaked: &Analytics) -> Text {
|
||||
let now = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.bus_arrivals(ui.primary.sim.time(), id);
|
||||
let baseline = prebaked.bus_arrivals(ui.primary.sim.time(), id);
|
||||
|
||||
let route = ui.primary.map.get_br(id);
|
||||
let mut txt = Text::new();
|
||||
txt.add(Line(format!("{} delay between stops", stat)));
|
||||
for idx1 in 0..route.stops.len() {
|
||||
let idx2 = if idx1 == route.stops.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
idx1 + 1
|
||||
};
|
||||
// TODO Also display number of arrivals...
|
||||
txt.add(Line(format!("Stop {}->{}: ", idx1 + 1, idx2 + 1)));
|
||||
if let Some(ref stats1) = now.get(&route.stops[idx2]) {
|
||||
let a = stats1.select(stat);
|
||||
txt.append(Line(a.minimal_tostring()));
|
||||
|
||||
if let Some(ref stats2) = baseline.get(&route.stops[idx2]) {
|
||||
txt.append_all(cmp_duration_shorter(a, stats2.select(stat)));
|
||||
}
|
||||
} else {
|
||||
txt.append(Line("no arrivals yet"));
|
||||
}
|
||||
}
|
||||
txt
|
||||
}
|
||||
|
||||
fn bus_delays(id: BusRouteID, ui: &UI, ctx: &mut EventCtx) -> Plot<Duration> {
|
||||
let route = ui.primary.map.get_br(id);
|
||||
let mut delays_per_stop = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.bus_arrivals_over_time(ui.primary.sim.time(), id);
|
||||
|
||||
let mut series = Vec::new();
|
||||
for idx1 in 0..route.stops.len() {
|
||||
let idx2 = if idx1 == route.stops.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
idx1 + 1
|
||||
};
|
||||
series.push(Series {
|
||||
label: format!("Stop {}->{}", idx1 + 1, idx2 + 1),
|
||||
color: rotating_color_total(idx1, route.stops.len()),
|
||||
pts: delays_per_stop
|
||||
.remove(&route.stops[idx2])
|
||||
.unwrap_or_else(Vec::new),
|
||||
});
|
||||
}
|
||||
Plot::new(
|
||||
&format!("delays for {}", route.name),
|
||||
series,
|
||||
Duration::ZERO,
|
||||
ctx,
|
||||
)
|
||||
}
|
||||
|
||||
fn gridlock_panel(ui: &UI, prebaked: &Analytics) -> Text {
|
||||
let (now_all, now_per_mode) = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.all_finished_trips(ui.primary.sim.time());
|
||||
let (baseline_all, baseline_per_mode) = prebaked.all_finished_trips(ui.primary.sim.time());
|
||||
|
||||
let mut txt = Text::new();
|
||||
txt.add_appended(vec![
|
||||
Line(format!(
|
||||
"{} total finished trips (",
|
||||
prettyprint_usize(now_all.count())
|
||||
)),
|
||||
cmp_count_fewer(now_all.count(), baseline_all.count()),
|
||||
Line(")"),
|
||||
]);
|
||||
|
||||
for mode in TripMode::all() {
|
||||
let a = now_per_mode[&mode].count();
|
||||
let b = baseline_per_mode[&mode].count();
|
||||
txt.add_appended(vec![
|
||||
Line(format!(" {}: {} (", mode, prettyprint_usize(a))),
|
||||
cmp_count_fewer(a, b),
|
||||
Line(")"),
|
||||
]);
|
||||
}
|
||||
|
||||
txt
|
||||
}
|
||||
|
||||
fn faster_trips_panel(mode: TripMode, ui: &UI, prebaked: &Analytics) -> Text {
|
||||
let now = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.finished_trips(ui.primary.sim.time(), mode);
|
||||
let baseline = prebaked.finished_trips(ui.primary.sim.time(), mode);
|
||||
|
||||
let mut txt = Text::new();
|
||||
txt.add_appended(vec![
|
||||
Line(format!(
|
||||
"{} finished {} trips (",
|
||||
prettyprint_usize(now.count()),
|
||||
mode
|
||||
)),
|
||||
cmp_count_more(now.count(), baseline.count()),
|
||||
Line(")"),
|
||||
]);
|
||||
if now.count() == 0 || baseline.count() == 0 {
|
||||
return txt;
|
||||
}
|
||||
|
||||
for stat in Statistic::all() {
|
||||
txt.add(Line(format!("{}: ", stat)));
|
||||
txt.append_all(cmp_duration_shorter(
|
||||
now.select(stat),
|
||||
baseline.select(stat),
|
||||
));
|
||||
}
|
||||
txt
|
||||
}
|
||||
|
||||
fn change_scenario(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
|
||||
let num_agents = ui.primary.current_flags.num_agents;
|
||||
let builtin = if let Some(n) = num_agents {
|
||||
format!("random scenario with {} agents", n)
|
||||
} else {
|
||||
"random scenario with some agents".to_string()
|
||||
};
|
||||
let scenario_name = wiz
|
||||
.wrap(ctx)
|
||||
.choose_string("Instantiate which scenario?", || {
|
||||
let mut list =
|
||||
abstutil::list_all_objects(abstutil::SCENARIOS, ui.primary.map.get_name());
|
||||
list.push(builtin.clone());
|
||||
list.push("just buses".to_string());
|
||||
list
|
||||
})?;
|
||||
Some(Transition::PopThenReplace(Box::new(SandboxMode::new(
|
||||
ctx,
|
||||
ui,
|
||||
GameplayMode::PlayScenario(scenario_name),
|
||||
))))
|
||||
}
|
||||
|
||||
fn load_map(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
|
||||
if let Some(name) = wiz.wrap(ctx).choose_string("Load which map?", || {
|
||||
let current_map = ui.primary.map.get_name();
|
||||
abstutil::list_all_objects("maps", "")
|
||||
.into_iter()
|
||||
.filter(|n| n != current_map)
|
||||
.collect()
|
||||
}) {
|
||||
ctx.canvas.save_camera_state(ui.primary.map.get_name());
|
||||
let mut flags = ui.primary.current_flags.clone();
|
||||
flags.sim_flags.load = abstutil::path_map(&name);
|
||||
*ui = UI::new(flags, ctx, false);
|
||||
Some(Transition::PopThenReplace(Box::new(SandboxMode::new(
|
||||
ctx,
|
||||
ui,
|
||||
// TODO If we were playing a scenario, load that one...
|
||||
GameplayMode::Freeform,
|
||||
))))
|
||||
} else if wiz.aborted() {
|
||||
Some(Transition::Pop)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Must call menu.event first. Returns true if the caller should set the overlay to the custom
|
||||
// thing.
|
||||
fn manage_overlays(
|
||||
menu: &mut ModalMenu,
|
||||
ctx: &mut EventCtx,
|
||||
show: &str,
|
||||
hide: &str,
|
||||
overlay: &mut Overlays,
|
||||
active_originally: bool,
|
||||
time_changed: bool,
|
||||
) -> bool {
|
||||
// Synchronize menus if needed. Player can change these separately.
|
||||
if active_originally {
|
||||
menu.maybe_change_action(show, hide, ctx);
|
||||
} else {
|
||||
menu.maybe_change_action(hide, show, ctx);
|
||||
}
|
||||
|
||||
if !active_originally && menu.swap_action(show, hide, ctx) {
|
||||
true
|
||||
} else if active_originally && menu.swap_action(hide, show, ctx) {
|
||||
*overlay = Overlays::Inactive;
|
||||
false
|
||||
} else {
|
||||
active_originally && time_changed
|
||||
}
|
||||
}
|
||||
|
||||
// Must call menu.event first.
|
||||
fn manage_acs(
|
||||
menu: &mut ModalMenu,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
show: &str,
|
||||
hide: &str,
|
||||
acs: AgentColorScheme,
|
||||
) {
|
||||
let active_originally = ui.agent_cs == acs;
|
||||
|
||||
// Synchronize menus if needed. Player can change these separately.
|
||||
if active_originally {
|
||||
menu.maybe_change_action(show, hide, ctx);
|
||||
} else {
|
||||
menu.maybe_change_action(hide, show, ctx);
|
||||
}
|
||||
|
||||
if !active_originally && menu.swap_action(show, hide, ctx) {
|
||||
ui.agent_cs = acs;
|
||||
} else if active_originally && menu.swap_action(hide, show, ctx) {
|
||||
ui.agent_cs = AgentColorScheme::VehicleTypes;
|
||||
}
|
||||
}
|
||||
|
||||
// Shorter is better
|
||||
fn cmp_duration_shorter(now: Duration, baseline: Duration) -> Vec<TextSpan> {
|
||||
if now.epsilon_eq(baseline) {
|
||||
vec![Line(" (same as baseline)")]
|
||||
} else if now < baseline {
|
||||
vec![
|
||||
Line(" ("),
|
||||
Line((baseline - now).minimal_tostring()).fg(Color::GREEN),
|
||||
Line(" faster)"),
|
||||
]
|
||||
} else if now > baseline {
|
||||
vec![
|
||||
Line(" ("),
|
||||
Line((now - baseline).minimal_tostring()).fg(Color::RED),
|
||||
Line(" slower)"),
|
||||
]
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// Fewer is better
|
||||
fn cmp_count_fewer(now: usize, baseline: usize) -> TextSpan {
|
||||
if now < baseline {
|
||||
Line(format!("{} fewer", prettyprint_usize(baseline - now))).fg(Color::GREEN)
|
||||
} else if now > baseline {
|
||||
Line(format!("{} more", prettyprint_usize(now - baseline))).fg(Color::RED)
|
||||
} else {
|
||||
Line("same as baseline")
|
||||
}
|
||||
}
|
||||
|
||||
// More is better
|
||||
fn cmp_count_more(now: usize, baseline: usize) -> TextSpan {
|
||||
if now < baseline {
|
||||
Line(format!("{} fewer", prettyprint_usize(baseline - now))).fg(Color::RED)
|
||||
} else if now > baseline {
|
||||
Line(format!("{} more", prettyprint_usize(now - baseline))).fg(Color::GREEN)
|
||||
} else {
|
||||
Line("same as baseline")
|
||||
}
|
||||
}
|
93
game/src/sandbox/gameplay/create_gridlock.rs
Normal file
93
game/src/sandbox/gameplay/create_gridlock.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use crate::game::{msg, Transition};
|
||||
use crate::render::AgentColorScheme;
|
||||
use crate::sandbox::gameplay::{cmp_count_fewer, manage_acs, State};
|
||||
use crate::ui::UI;
|
||||
use abstutil::prettyprint_usize;
|
||||
use ezgui::{hotkey, EventCtx, Key, Line, ModalMenu, Text};
|
||||
use geom::Duration;
|
||||
use sim::{Analytics, TripMode};
|
||||
|
||||
pub struct CreateGridlock {
|
||||
time: Duration,
|
||||
}
|
||||
|
||||
impl CreateGridlock {
|
||||
pub fn new(ctx: &EventCtx) -> (ModalMenu, State) {
|
||||
(
|
||||
ModalMenu::new(
|
||||
"Cause gridlock",
|
||||
vec![
|
||||
(hotkey(Key::E), "show agent delay"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
),
|
||||
State::CreateGridlock(CreateGridlock {
|
||||
time: Duration::ZERO,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
menu: &mut ModalMenu,
|
||||
prebaked: &Analytics,
|
||||
) -> Option<Transition> {
|
||||
menu.event(ctx);
|
||||
manage_acs(
|
||||
menu,
|
||||
ctx,
|
||||
ui,
|
||||
"show agent delay",
|
||||
"hide agent delay",
|
||||
AgentColorScheme::Delay,
|
||||
);
|
||||
|
||||
if self.time != ui.primary.sim.time() {
|
||||
self.time = ui.primary.sim.time();
|
||||
menu.set_info(ctx, gridlock_panel(ui, prebaked));
|
||||
}
|
||||
|
||||
if menu.action("help") {
|
||||
return Some(Transition::Push(msg("Help", vec![
|
||||
"You might notice a few places in the map where gridlock forms already.",
|
||||
"You can make things worse!",
|
||||
"How few lanes can you close for construction before everything grinds to a halt?",
|
||||
])));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn gridlock_panel(ui: &UI, prebaked: &Analytics) -> Text {
|
||||
let (now_all, now_per_mode) = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.all_finished_trips(ui.primary.sim.time());
|
||||
let (baseline_all, baseline_per_mode) = prebaked.all_finished_trips(ui.primary.sim.time());
|
||||
|
||||
let mut txt = Text::new();
|
||||
txt.add_appended(vec![
|
||||
Line(format!(
|
||||
"{} total finished trips (",
|
||||
prettyprint_usize(now_all.count())
|
||||
)),
|
||||
cmp_count_fewer(now_all.count(), baseline_all.count()),
|
||||
Line(")"),
|
||||
]);
|
||||
|
||||
for mode in TripMode::all() {
|
||||
let a = now_per_mode[&mode].count();
|
||||
let b = baseline_per_mode[&mode].count();
|
||||
txt.add_appended(vec![
|
||||
Line(format!(" {}: {} (", mode, prettyprint_usize(a))),
|
||||
cmp_count_fewer(a, b),
|
||||
Line(")"),
|
||||
]);
|
||||
}
|
||||
|
||||
txt
|
||||
}
|
83
game/src/sandbox/gameplay/faster_trips.rs
Normal file
83
game/src/sandbox/gameplay/faster_trips.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use crate::game::{msg, Transition};
|
||||
use crate::sandbox::gameplay::{cmp_count_more, cmp_duration_shorter, State};
|
||||
use crate::ui::UI;
|
||||
use abstutil::prettyprint_usize;
|
||||
use ezgui::{hotkey, EventCtx, Key, Line, ModalMenu, Text};
|
||||
use geom::{Duration, Statistic};
|
||||
use sim::{Analytics, TripMode};
|
||||
|
||||
pub struct FasterTrips {
|
||||
mode: TripMode,
|
||||
time: Duration,
|
||||
}
|
||||
|
||||
impl FasterTrips {
|
||||
pub fn new(trip_mode: TripMode, ctx: &EventCtx) -> (ModalMenu, State) {
|
||||
(
|
||||
ModalMenu::new(
|
||||
&format!("Speed up {} trips", trip_mode),
|
||||
vec![(hotkey(Key::H), "help")],
|
||||
ctx,
|
||||
),
|
||||
State::FasterTrips(FasterTrips {
|
||||
mode: trip_mode,
|
||||
time: Duration::ZERO,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
menu: &mut ModalMenu,
|
||||
prebaked: &Analytics,
|
||||
) -> Option<Transition> {
|
||||
menu.event(ctx);
|
||||
|
||||
if self.time != ui.primary.sim.time() {
|
||||
self.time = ui.primary.sim.time();
|
||||
menu.set_info(ctx, faster_trips_panel(self.mode, ui, prebaked));
|
||||
}
|
||||
|
||||
if menu.action("help") {
|
||||
return Some(Transition::Push(msg(
|
||||
"Help",
|
||||
vec!["How can you possibly speed up all trips of some mode?"],
|
||||
)));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn faster_trips_panel(mode: TripMode, ui: &UI, prebaked: &Analytics) -> Text {
|
||||
let now = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.finished_trips(ui.primary.sim.time(), mode);
|
||||
let baseline = prebaked.finished_trips(ui.primary.sim.time(), mode);
|
||||
|
||||
let mut txt = Text::new();
|
||||
txt.add_appended(vec![
|
||||
Line(format!(
|
||||
"{} finished {} trips (",
|
||||
prettyprint_usize(now.count()),
|
||||
mode
|
||||
)),
|
||||
cmp_count_more(now.count(), baseline.count()),
|
||||
Line(")"),
|
||||
]);
|
||||
if now.count() == 0 || baseline.count() == 0 {
|
||||
return txt;
|
||||
}
|
||||
|
||||
for stat in Statistic::all() {
|
||||
txt.add(Line(format!("{}: ", stat)));
|
||||
txt.append_all(cmp_duration_shorter(
|
||||
now.select(stat),
|
||||
baseline.select(stat),
|
||||
));
|
||||
}
|
||||
txt
|
||||
}
|
52
game/src/sandbox/gameplay/freeform.rs
Normal file
52
game/src/sandbox/gameplay/freeform.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::game::{msg, Transition, WizardState};
|
||||
use crate::sandbox::gameplay::{change_scenario, load_map, State};
|
||||
use crate::sandbox::spawner;
|
||||
use crate::ui::UI;
|
||||
use ezgui::{hotkey, lctrl, EventCtx, Key, ModalMenu};
|
||||
|
||||
// TODO Maybe remember what things were spawned, offer to replay this later
|
||||
pub struct Freeform;
|
||||
|
||||
impl Freeform {
|
||||
pub fn new(ctx: &EventCtx) -> (ModalMenu, State) {
|
||||
(
|
||||
ModalMenu::new(
|
||||
"Freeform mode",
|
||||
vec![
|
||||
(hotkey(Key::S), "start a scenario"),
|
||||
(lctrl(Key::L), "load another map"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
),
|
||||
State::Freeform(Freeform),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
menu: &mut ModalMenu,
|
||||
) -> Option<Transition> {
|
||||
menu.event(ctx);
|
||||
if menu.action("start a scenario") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
change_scenario,
|
||||
))));
|
||||
}
|
||||
if menu.action("load another map") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(load_map))));
|
||||
}
|
||||
if menu.action("help") {
|
||||
return Some(Transition::Push(msg("Help", vec!["This simulation is empty by default.", "Try right-clicking an intersection and choosing to spawn agents (or just hover over it and press Z).", "You can also spawn agents from buildings or lanes.", "You can also start a full scenario to get realistic traffic."])));
|
||||
}
|
||||
if let Some(new_state) = spawner::AgentSpawner::new(ctx, ui) {
|
||||
return Some(Transition::Push(new_state));
|
||||
}
|
||||
if let Some(new_state) = spawner::SpawnManyAgents::new(ctx, ui) {
|
||||
return Some(Transition::Push(new_state));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
305
game/src/sandbox/gameplay/mod.rs
Normal file
305
game/src/sandbox/gameplay/mod.rs
Normal file
@ -0,0 +1,305 @@
|
||||
mod create_gridlock;
|
||||
mod faster_trips;
|
||||
mod freeform;
|
||||
mod optimize_bus;
|
||||
mod play_scenario;
|
||||
|
||||
use crate::game::Transition;
|
||||
use crate::render::AgentColorScheme;
|
||||
use crate::sandbox::overlays::Overlays;
|
||||
use crate::sandbox::SandboxMode;
|
||||
use crate::ui::UI;
|
||||
use abstutil::{prettyprint_usize, Timer};
|
||||
use ezgui::{Color, EventCtx, GfxCtx, Line, ModalMenu, TextSpan, Wizard};
|
||||
use geom::Duration;
|
||||
use sim::{Analytics, Scenario, TripMode};
|
||||
|
||||
pub struct GameplayRunner {
|
||||
pub mode: GameplayMode,
|
||||
pub menu: ModalMenu,
|
||||
state: State,
|
||||
prebaked: Analytics,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum GameplayMode {
|
||||
// TODO Maybe this should be "sandbox"
|
||||
Freeform,
|
||||
PlayScenario(String),
|
||||
// Route name
|
||||
OptimizeBus(String),
|
||||
CreateGridlock,
|
||||
// TODO Be able to filter population by more factors
|
||||
FasterTrips(TripMode),
|
||||
}
|
||||
|
||||
pub enum State {
|
||||
Freeform(freeform::Freeform),
|
||||
PlayScenario(play_scenario::PlayScenario),
|
||||
OptimizeBus(optimize_bus::OptimizeBus),
|
||||
CreateGridlock(create_gridlock::CreateGridlock),
|
||||
FasterTrips(faster_trips::FasterTrips),
|
||||
}
|
||||
|
||||
impl GameplayRunner {
|
||||
pub fn initialize(mode: GameplayMode, ui: &mut UI, ctx: &mut EventCtx) -> GameplayRunner {
|
||||
let prebaked: Analytics = abstutil::read_binary(
|
||||
&abstutil::path_prebaked_results(ui.primary.map.get_name()),
|
||||
&mut Timer::throwaway(),
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
println!("WARNING! No prebaked sim analytics. Only freeform mode will work.");
|
||||
Analytics::new()
|
||||
});
|
||||
|
||||
let ((menu, state), maybe_scenario) = match mode.clone() {
|
||||
GameplayMode::Freeform => (freeform::Freeform::new(ctx), None),
|
||||
GameplayMode::PlayScenario(scenario) => (
|
||||
play_scenario::PlayScenario::new(&scenario, ctx),
|
||||
Some(scenario),
|
||||
),
|
||||
GameplayMode::OptimizeBus(route_name) => (
|
||||
optimize_bus::OptimizeBus::new(route_name, ctx, ui),
|
||||
Some("weekday_typical_traffic_from_psrc".to_string()),
|
||||
),
|
||||
GameplayMode::CreateGridlock => (
|
||||
create_gridlock::CreateGridlock::new(ctx),
|
||||
Some("weekday_typical_traffic_from_psrc".to_string()),
|
||||
),
|
||||
GameplayMode::FasterTrips(trip_mode) => (
|
||||
faster_trips::FasterTrips::new(trip_mode, ctx),
|
||||
Some("weekday_typical_traffic_from_psrc".to_string()),
|
||||
),
|
||||
};
|
||||
let runner = GameplayRunner {
|
||||
mode,
|
||||
menu: menu.disable_standalone_layout(),
|
||||
state,
|
||||
prebaked,
|
||||
};
|
||||
if let Some(scenario_name) = maybe_scenario {
|
||||
ctx.loading_screen("instantiate scenario", |_, timer| {
|
||||
let num_agents = ui.primary.current_flags.num_agents;
|
||||
let builtin = if let Some(n) = num_agents {
|
||||
format!("random scenario with {} agents", n)
|
||||
} else {
|
||||
"random scenario with some agents".to_string()
|
||||
};
|
||||
let scenario = if scenario_name == builtin {
|
||||
if let Some(n) = num_agents {
|
||||
Scenario::scaled_run(&ui.primary.map, n)
|
||||
} else {
|
||||
Scenario::small_run(&ui.primary.map)
|
||||
}
|
||||
} else if scenario_name == "just buses" {
|
||||
let mut s = Scenario::empty(&ui.primary.map);
|
||||
s.scenario_name = "just buses".to_string();
|
||||
s.seed_buses = true;
|
||||
s
|
||||
} else {
|
||||
abstutil::read_binary(
|
||||
&abstutil::path1_bin(
|
||||
&ui.primary.map.get_name(),
|
||||
abstutil::SCENARIOS,
|
||||
&scenario_name,
|
||||
),
|
||||
timer,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
scenario.instantiate(
|
||||
&mut ui.primary.sim,
|
||||
&ui.primary.map,
|
||||
&mut ui.primary.current_flags.sim_flags.make_rng(),
|
||||
timer,
|
||||
);
|
||||
ui.primary.sim.step(&ui.primary.map, Duration::seconds(0.1));
|
||||
});
|
||||
}
|
||||
runner
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
overlays: &mut Overlays,
|
||||
) -> Option<Transition> {
|
||||
match self.state {
|
||||
State::Freeform(ref mut f) => {
|
||||
if let Some(t) = f.event(ctx, ui, &mut self.menu) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
State::PlayScenario(ref mut p) => {
|
||||
if let Some(t) = p.event(ctx, ui, &mut self.menu) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
State::OptimizeBus(ref mut o) => {
|
||||
if let Some(t) = o.event(ctx, ui, overlays, &mut self.menu, &self.prebaked) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
State::CreateGridlock(ref mut g) => {
|
||||
if let Some(t) = g.event(ctx, ui, &mut self.menu, &self.prebaked) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
State::FasterTrips(ref mut f) => {
|
||||
if let Some(t) = f.event(ctx, ui, &mut self.menu, &self.prebaked) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn draw(&self, g: &mut GfxCtx) {
|
||||
self.menu.draw(g);
|
||||
}
|
||||
}
|
||||
|
||||
fn change_scenario(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
|
||||
let num_agents = ui.primary.current_flags.num_agents;
|
||||
let builtin = if let Some(n) = num_agents {
|
||||
format!("random scenario with {} agents", n)
|
||||
} else {
|
||||
"random scenario with some agents".to_string()
|
||||
};
|
||||
let scenario_name = wiz
|
||||
.wrap(ctx)
|
||||
.choose_string("Instantiate which scenario?", || {
|
||||
let mut list =
|
||||
abstutil::list_all_objects(abstutil::SCENARIOS, ui.primary.map.get_name());
|
||||
list.push(builtin.clone());
|
||||
list.push("just buses".to_string());
|
||||
list
|
||||
})?;
|
||||
Some(Transition::PopThenReplace(Box::new(SandboxMode::new(
|
||||
ctx,
|
||||
ui,
|
||||
GameplayMode::PlayScenario(scenario_name),
|
||||
))))
|
||||
}
|
||||
|
||||
fn load_map(wiz: &mut Wizard, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
|
||||
if let Some(name) = wiz.wrap(ctx).choose_string("Load which map?", || {
|
||||
let current_map = ui.primary.map.get_name();
|
||||
abstutil::list_all_objects("maps", "")
|
||||
.into_iter()
|
||||
.filter(|n| n != current_map)
|
||||
.collect()
|
||||
}) {
|
||||
ctx.canvas.save_camera_state(ui.primary.map.get_name());
|
||||
let mut flags = ui.primary.current_flags.clone();
|
||||
flags.sim_flags.load = abstutil::path_map(&name);
|
||||
*ui = UI::new(flags, ctx, false);
|
||||
Some(Transition::PopThenReplace(Box::new(SandboxMode::new(
|
||||
ctx,
|
||||
ui,
|
||||
// TODO If we were playing a scenario, load that one...
|
||||
GameplayMode::Freeform,
|
||||
))))
|
||||
} else if wiz.aborted() {
|
||||
Some(Transition::Pop)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Must call menu.event first. Returns true if the caller should set the overlay to the custom
|
||||
// thing.
|
||||
fn manage_overlays(
|
||||
menu: &mut ModalMenu,
|
||||
ctx: &mut EventCtx,
|
||||
show: &str,
|
||||
hide: &str,
|
||||
overlay: &mut Overlays,
|
||||
active_originally: bool,
|
||||
time_changed: bool,
|
||||
) -> bool {
|
||||
// Synchronize menus if needed. Player can change these separately.
|
||||
if active_originally {
|
||||
menu.maybe_change_action(show, hide, ctx);
|
||||
} else {
|
||||
menu.maybe_change_action(hide, show, ctx);
|
||||
}
|
||||
|
||||
if !active_originally && menu.swap_action(show, hide, ctx) {
|
||||
true
|
||||
} else if active_originally && menu.swap_action(hide, show, ctx) {
|
||||
*overlay = Overlays::Inactive;
|
||||
false
|
||||
} else {
|
||||
active_originally && time_changed
|
||||
}
|
||||
}
|
||||
|
||||
// Must call menu.event first.
|
||||
fn manage_acs(
|
||||
menu: &mut ModalMenu,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
show: &str,
|
||||
hide: &str,
|
||||
acs: AgentColorScheme,
|
||||
) {
|
||||
let active_originally = ui.agent_cs == acs;
|
||||
|
||||
// Synchronize menus if needed. Player can change these separately.
|
||||
if active_originally {
|
||||
menu.maybe_change_action(show, hide, ctx);
|
||||
} else {
|
||||
menu.maybe_change_action(hide, show, ctx);
|
||||
}
|
||||
|
||||
if !active_originally && menu.swap_action(show, hide, ctx) {
|
||||
ui.agent_cs = acs;
|
||||
} else if active_originally && menu.swap_action(hide, show, ctx) {
|
||||
ui.agent_cs = AgentColorScheme::VehicleTypes;
|
||||
}
|
||||
}
|
||||
|
||||
// Shorter is better
|
||||
fn cmp_duration_shorter(now: Duration, baseline: Duration) -> Vec<TextSpan> {
|
||||
if now.epsilon_eq(baseline) {
|
||||
vec![Line(" (same as baseline)")]
|
||||
} else if now < baseline {
|
||||
vec![
|
||||
Line(" ("),
|
||||
Line((baseline - now).minimal_tostring()).fg(Color::GREEN),
|
||||
Line(" faster)"),
|
||||
]
|
||||
} else if now > baseline {
|
||||
vec![
|
||||
Line(" ("),
|
||||
Line((now - baseline).minimal_tostring()).fg(Color::RED),
|
||||
Line(" slower)"),
|
||||
]
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
// Fewer is better
|
||||
fn cmp_count_fewer(now: usize, baseline: usize) -> TextSpan {
|
||||
if now < baseline {
|
||||
Line(format!("{} fewer", prettyprint_usize(baseline - now))).fg(Color::GREEN)
|
||||
} else if now > baseline {
|
||||
Line(format!("{} more", prettyprint_usize(now - baseline))).fg(Color::RED)
|
||||
} else {
|
||||
Line("same as baseline")
|
||||
}
|
||||
}
|
||||
|
||||
// More is better
|
||||
fn cmp_count_more(now: usize, baseline: usize) -> TextSpan {
|
||||
if now < baseline {
|
||||
Line(format!("{} fewer", prettyprint_usize(baseline - now))).fg(Color::RED)
|
||||
} else if now > baseline {
|
||||
Line(format!("{} more", prettyprint_usize(now - baseline))).fg(Color::GREEN)
|
||||
} else {
|
||||
Line("same as baseline")
|
||||
}
|
||||
}
|
193
game/src/sandbox/gameplay/optimize_bus.rs
Normal file
193
game/src/sandbox/gameplay/optimize_bus.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use crate::common::{Plot, Series};
|
||||
use crate::game::{msg, Transition, WizardState};
|
||||
use crate::helpers::rotating_color_total;
|
||||
use crate::sandbox::gameplay::{cmp_duration_shorter, manage_overlays, State};
|
||||
use crate::sandbox::overlays::Overlays;
|
||||
use crate::sandbox::{bus_explorer, SandboxMode};
|
||||
use crate::ui::UI;
|
||||
use ezgui::{hotkey, Choice, EventCtx, Key, Line, ModalMenu, Text};
|
||||
use geom::{Duration, Statistic};
|
||||
use map_model::BusRouteID;
|
||||
use sim::Analytics;
|
||||
|
||||
pub struct OptimizeBus {
|
||||
route: BusRouteID,
|
||||
time: Duration,
|
||||
stat: Statistic,
|
||||
}
|
||||
|
||||
impl OptimizeBus {
|
||||
pub fn new(route_name: String, ctx: &EventCtx, ui: &UI) -> (ModalMenu, State) {
|
||||
let route = ui.primary.map.get_bus_route(&route_name).unwrap();
|
||||
(
|
||||
ModalMenu::new(
|
||||
&format!("Optimize {}", route_name),
|
||||
vec![
|
||||
(hotkey(Key::E), "show bus route"),
|
||||
(hotkey(Key::T), "show delays over time"),
|
||||
(hotkey(Key::S), "change statistic"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
),
|
||||
State::OptimizeBus(OptimizeBus {
|
||||
route: route.id,
|
||||
time: Duration::ZERO,
|
||||
stat: Statistic::Max,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
ui: &mut UI,
|
||||
overlays: &mut Overlays,
|
||||
menu: &mut ModalMenu,
|
||||
prebaked: &Analytics,
|
||||
) -> Option<Transition> {
|
||||
menu.event(ctx);
|
||||
if manage_overlays(
|
||||
menu,
|
||||
ctx,
|
||||
"show bus route",
|
||||
"hide bus route",
|
||||
overlays,
|
||||
match overlays {
|
||||
Overlays::BusRoute(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
self.time != ui.primary.sim.time(),
|
||||
) {
|
||||
*overlays = Overlays::BusRoute(bus_explorer::ShowBusRoute::new(
|
||||
ui.primary.map.get_br(self.route),
|
||||
ui,
|
||||
ctx,
|
||||
));
|
||||
}
|
||||
if manage_overlays(
|
||||
menu,
|
||||
ctx,
|
||||
"show delays over time",
|
||||
"hide delays over time",
|
||||
overlays,
|
||||
match overlays {
|
||||
Overlays::BusDelaysOverTime(_) => true,
|
||||
_ => false,
|
||||
},
|
||||
self.time != ui.primary.sim.time(),
|
||||
) {
|
||||
*overlays = Overlays::BusDelaysOverTime(bus_delays(self.route, ui, ctx));
|
||||
}
|
||||
|
||||
// TODO Expensive
|
||||
if self.time != ui.primary.sim.time() {
|
||||
self.time = ui.primary.sim.time();
|
||||
menu.set_info(ctx, bus_route_panel(self.route, ui, self.stat, prebaked));
|
||||
}
|
||||
|
||||
if menu.action("change statistic") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
move |wiz, ctx, _| {
|
||||
// TODO Filter out existing. Make this kind of thing much easier.
|
||||
let (_, new_stat) = wiz.wrap(ctx).choose(
|
||||
"Show which statistic on frequency a bus stop is visited?",
|
||||
|| {
|
||||
Statistic::all()
|
||||
.into_iter()
|
||||
.map(|s| Choice::new(s.to_string(), s))
|
||||
.collect()
|
||||
},
|
||||
)?;
|
||||
Some(Transition::PopWithData(Box::new(move |state, _, _| {
|
||||
let sandbox = state.downcast_mut::<SandboxMode>().unwrap();
|
||||
match sandbox.gameplay.state {
|
||||
State::OptimizeBus(ref mut o) => {
|
||||
// Force recalculation
|
||||
o.time = Duration::ZERO;
|
||||
o.stat = new_stat;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
})))
|
||||
},
|
||||
))));
|
||||
}
|
||||
if menu.action("help") {
|
||||
return Some(Transition::Push(msg(
|
||||
"Help",
|
||||
vec![
|
||||
"First find where the bus gets stuck.",
|
||||
"Then use edit mode to try to speed things up.",
|
||||
"Try making dedicated bus lanes",
|
||||
"and adjusting traffic signals.",
|
||||
],
|
||||
)));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn bus_route_panel(id: BusRouteID, ui: &UI, stat: Statistic, prebaked: &Analytics) -> Text {
|
||||
let now = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.bus_arrivals(ui.primary.sim.time(), id);
|
||||
let baseline = prebaked.bus_arrivals(ui.primary.sim.time(), id);
|
||||
|
||||
let route = ui.primary.map.get_br(id);
|
||||
let mut txt = Text::new();
|
||||
txt.add(Line(format!("{} delay between stops", stat)));
|
||||
for idx1 in 0..route.stops.len() {
|
||||
let idx2 = if idx1 == route.stops.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
idx1 + 1
|
||||
};
|
||||
// TODO Also display number of arrivals...
|
||||
txt.add(Line(format!("Stop {}->{}: ", idx1 + 1, idx2 + 1)));
|
||||
if let Some(ref stats1) = now.get(&route.stops[idx2]) {
|
||||
let a = stats1.select(stat);
|
||||
txt.append(Line(a.minimal_tostring()));
|
||||
|
||||
if let Some(ref stats2) = baseline.get(&route.stops[idx2]) {
|
||||
txt.append_all(cmp_duration_shorter(a, stats2.select(stat)));
|
||||
}
|
||||
} else {
|
||||
txt.append(Line("no arrivals yet"));
|
||||
}
|
||||
}
|
||||
txt
|
||||
}
|
||||
|
||||
fn bus_delays(id: BusRouteID, ui: &UI, ctx: &mut EventCtx) -> Plot<Duration> {
|
||||
let route = ui.primary.map.get_br(id);
|
||||
let mut delays_per_stop = ui
|
||||
.primary
|
||||
.sim
|
||||
.get_analytics()
|
||||
.bus_arrivals_over_time(ui.primary.sim.time(), id);
|
||||
|
||||
let mut series = Vec::new();
|
||||
for idx1 in 0..route.stops.len() {
|
||||
let idx2 = if idx1 == route.stops.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
idx1 + 1
|
||||
};
|
||||
series.push(Series {
|
||||
label: format!("Stop {}->{}", idx1 + 1, idx2 + 1),
|
||||
color: rotating_color_total(idx1, route.stops.len()),
|
||||
pts: delays_per_stop
|
||||
.remove(&route.stops[idx2])
|
||||
.unwrap_or_else(Vec::new),
|
||||
});
|
||||
}
|
||||
Plot::new(
|
||||
&format!("delays for {}", route.name),
|
||||
series,
|
||||
Duration::ZERO,
|
||||
ctx,
|
||||
)
|
||||
}
|
51
game/src/sandbox/gameplay/play_scenario.rs
Normal file
51
game/src/sandbox/gameplay/play_scenario.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use crate::game::{msg, Transition, WizardState};
|
||||
use crate::sandbox::gameplay::{change_scenario, load_map, State};
|
||||
use crate::ui::UI;
|
||||
use ezgui::{hotkey, lctrl, EventCtx, Key, ModalMenu};
|
||||
|
||||
pub struct PlayScenario;
|
||||
|
||||
impl PlayScenario {
|
||||
pub fn new(name: &String, ctx: &EventCtx) -> (ModalMenu, State) {
|
||||
(
|
||||
ModalMenu::new(
|
||||
&format!("Playing {}", name),
|
||||
vec![
|
||||
(hotkey(Key::S), "start another scenario"),
|
||||
(lctrl(Key::L), "load another map"),
|
||||
(hotkey(Key::H), "help"),
|
||||
],
|
||||
ctx,
|
||||
),
|
||||
State::PlayScenario(PlayScenario),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn event(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
_ui: &UI,
|
||||
menu: &mut ModalMenu,
|
||||
) -> Option<Transition> {
|
||||
menu.event(ctx);
|
||||
if menu.action("start another scenario") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(
|
||||
change_scenario,
|
||||
))));
|
||||
}
|
||||
if menu.action("load another map") {
|
||||
return Some(Transition::Push(WizardState::new(Box::new(load_map))));
|
||||
}
|
||||
if menu.action("help") {
|
||||
return Some(Transition::Push(msg(
|
||||
"Help",
|
||||
vec![
|
||||
"Do things seem a bit quiet?",
|
||||
"The simulation starts at midnight, so you might need to wait a bit.",
|
||||
"Try using the speed controls on the left.",
|
||||
],
|
||||
)));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ pub struct SandboxMode {
|
||||
save_tools: MenuUnderButton,
|
||||
agent_tools: AgentTools,
|
||||
overlay: overlays::Overlays,
|
||||
gameplay: gameplay::GameplayState,
|
||||
gameplay: gameplay::GameplayRunner,
|
||||
common: CommonState,
|
||||
menu: ModalMenu,
|
||||
}
|
||||
@ -71,7 +71,7 @@ impl SandboxMode {
|
||||
),
|
||||
agent_tools: AgentTools::new(),
|
||||
overlay: overlays::Overlays::Inactive,
|
||||
gameplay: gameplay::GameplayState::initialize(mode, ui, ctx),
|
||||
gameplay: gameplay::GameplayRunner::initialize(mode, ui, ctx),
|
||||
common: CommonState::new(ctx),
|
||||
menu: ModalMenu::new(
|
||||
"Sandbox Mode",
|
||||
|
Loading…
Reference in New Issue
Block a user