make a second part to the optimize commute challenge. just optimize two separate people. [rebuild]

This commit is contained in:
Dustin Carlino 2020-04-19 14:55:24 -07:00
parent 5cf0b72bac
commit 7bf899cab8
6 changed files with 201 additions and 97 deletions

View File

@ -13,7 +13,7 @@ use geom::{Bounds, Circle, Distance, Pt2D};
use map_model::{Map, Traversable};
use rand::seq::SliceRandom;
use sim::{Analytics, GetDrawAgents, Sim, SimFlags};
use std::collections::HashMap;
use std::collections::BTreeMap;
pub struct App {
pub primary: PerMap,
@ -499,14 +499,14 @@ impl PerMap {
// TODO Serialize these, but in a very careful, future-compatible way
pub struct SessionState {
pub tutorial: Option<TutorialState>,
pub high_scores: HashMap<GameplayMode, Vec<HighScore>>,
pub high_scores: BTreeMap<GameplayMode, Vec<HighScore>>,
}
impl SessionState {
pub fn empty() -> SessionState {
SessionState {
tutorial: None,
high_scores: HashMap::new(),
high_scores: BTreeMap::new(),
}
}
}

View File

@ -15,7 +15,7 @@ pub struct Challenge {
pub description: Vec<String>,
pub alias: String,
pub gameplay: GameplayMode,
cutscene: Option<fn(&mut EventCtx, &App) -> Box<dyn State>>,
pub cutscene: Option<fn(&mut EventCtx, &App) -> Box<dyn State>>,
}
// TODO Assuming the measurement is always maximizing time savings from a goal.
@ -25,69 +25,105 @@ pub struct HighScore {
pub edits_name: String,
}
pub fn all_challenges(dev: bool) -> BTreeMap<String, Vec<Challenge>> {
let mut tree = BTreeMap::new();
tree.insert(
"Optimize one commute".to_string(),
vec![Challenge {
title: "Part 1".to_string(),
description: vec!["Speed up one VIP's daily commute, at any cost!".to_string()],
alias: "commute/pt1".to_string(),
gameplay: GameplayMode::OptimizeCommute(PersonID(3434)),
cutscene: Some(crate::sandbox::gameplay::commute::OptimizeCommute::cutscene),
}],
);
if dev {
impl Challenge {
pub fn all(dev: bool) -> BTreeMap<String, Vec<Challenge>> {
let mut tree = BTreeMap::new();
tree.insert(
"Fix traffic signals".to_string(),
"Optimize one commute".to_string(),
vec![
Challenge {
title: "Tutorial 1".to_string(),
description: vec!["Add or remove a dedicated left phase".to_string()],
alias: "trafficsig/tut1".to_string(),
gameplay: GameplayMode::FixTrafficSignalsTutorial(0),
cutscene: None,
title: "Part 1".to_string(),
description: vec!["Speed up one VIP's daily commute, at any cost!".to_string()],
alias: "commute/pt1".to_string(),
gameplay: GameplayMode::OptimizeCommute(PersonID(1163), Duration::minutes(2)),
cutscene: Some(
crate::sandbox::gameplay::commute::OptimizeCommute::cutscene_pt1,
),
},
Challenge {
title: "Tutorial 2".to_string(),
description: vec!["Deal with heavy foot traffic".to_string()],
alias: "trafficsig/tut2".to_string(),
gameplay: GameplayMode::FixTrafficSignalsTutorial(1),
cutscene: None,
},
Challenge {
title: "The real challenge!".to_string(),
description: vec![
"A city-wide power surge knocked out all of the traffic signals!"
.to_string(),
"Their timing has been reset to default settings, and drivers are stuck."
.to_string(),
"It's up to you to repair the signals, choosing appropriate turn phases \
and timing."
.to_string(),
"".to_string(),
"Objective: Reduce the average trip time by at least 30s".to_string(),
],
alias: "trafficsig/main".to_string(),
gameplay: GameplayMode::FixTrafficSignals,
cutscene: None,
title: "Part 2".to_string(),
description: vec!["Speed up another VIP's commute".to_string()],
alias: "commute/pt2".to_string(),
gameplay: GameplayMode::OptimizeCommute(PersonID(3434), Duration::minutes(5)),
cutscene: Some(
crate::sandbox::gameplay::commute::OptimizeCommute::cutscene_pt2,
),
},
],
);
tree.insert(
"Cause gridlock (WIP)".to_string(),
vec![Challenge {
title: "Gridlock all of the everything".to_string(),
description: vec!["Make traffic as BAD as possible!".to_string()],
alias: "gridlock".to_string(),
gameplay: GameplayMode::CreateGridlock(abstutil::path_map("montlake")),
cutscene: None,
}],
);
if dev {
tree.insert(
"Fix traffic signals".to_string(),
vec![
Challenge {
title: "Tutorial 1".to_string(),
description: vec!["Add or remove a dedicated left phase".to_string()],
alias: "trafficsig/tut1".to_string(),
gameplay: GameplayMode::FixTrafficSignalsTutorial(0),
cutscene: None,
},
Challenge {
title: "Tutorial 2".to_string(),
description: vec!["Deal with heavy foot traffic".to_string()],
alias: "trafficsig/tut2".to_string(),
gameplay: GameplayMode::FixTrafficSignalsTutorial(1),
cutscene: None,
},
Challenge {
title: "The real challenge!".to_string(),
description: vec![
"A city-wide power surge knocked out all of the traffic signals!"
.to_string(),
"Their timing has been reset to default settings, and drivers are \
stuck."
.to_string(),
"It's up to you to repair the signals, choosing appropriate turn \
phases and timing."
.to_string(),
"".to_string(),
"Objective: Reduce the average trip time by at least 30s".to_string(),
],
alias: "trafficsig/main".to_string(),
gameplay: GameplayMode::FixTrafficSignals,
cutscene: None,
},
],
);
tree.insert(
"Cause gridlock (WIP)".to_string(),
vec![Challenge {
title: "Gridlock all of the everything".to_string(),
description: vec!["Make traffic as BAD as possible!".to_string()],
alias: "gridlock".to_string(),
gameplay: GameplayMode::CreateGridlock(abstutil::path_map("montlake")),
cutscene: None,
}],
);
}
tree
}
// Also returns the next stage, if there is one
pub fn find(mode: &GameplayMode) -> (Challenge, Option<Challenge>) {
// Find the next stage
for (_, stages) in Challenge::all(true) {
let mut current = None;
for challenge in stages {
if current.is_some() {
return (current.unwrap(), Some(challenge));
}
if &challenge.gameplay == mode {
current = Some(challenge);
}
}
if let Some(c) = current {
return (c, None);
}
}
unreachable!()
}
tree
}
pub fn challenges_picker(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State> {
@ -117,7 +153,7 @@ impl Tab {
// First list challenges
let mut flex_row = Vec::new();
for (idx, (name, _)) in all_challenges(app.opts.dev).into_iter().enumerate() {
for (idx, (name, _)) in Challenge::all(app.opts.dev).into_iter().enumerate() {
let current = match self {
Tab::NothingChosen => false,
Tab::ChallengeStage(ref n, _) => &name == n,
@ -154,7 +190,7 @@ impl Tab {
// List stages
if let Tab::ChallengeStage(ref name, current) = self {
let mut col = Vec::new();
for (idx, stage) in all_challenges(app.opts.dev)
for (idx, stage) in Challenge::all(app.opts.dev)
.remove(name)
.unwrap()
.into_iter()
@ -186,7 +222,7 @@ impl Tab {
// Describe the specific stage
if let Tab::ChallengeStage(ref name, current) = self {
let challenge = all_challenges(app.opts.dev)
let challenge = Challenge::all(app.opts.dev)
.remove(name)
.unwrap()
.remove(current);
@ -257,7 +293,7 @@ pub fn prebake_all() {
let mut timer = Timer::new("prebake all challenge results");
let mut per_map: BTreeMap<String, Vec<Challenge>> = BTreeMap::new();
for (_, list) in all_challenges(true) {
for (_, list) in Challenge::all(true) {
for c in list {
per_map
.entry(c.gameplay.map_path())

View File

@ -83,7 +83,7 @@ fn main() {
let mut mode = None;
if let Some(x) = args.optional("--challenge") {
let mut aliases = Vec::new();
'OUTER: for (_, stages) in challenges::all_challenges(true) {
'OUTER: for (_, stages) in challenges::Challenge::all(true) {
for challenge in stages {
if challenge.alias == x {
flags.sim_flags.load = challenge.gameplay.map_path();

View File

@ -1,5 +1,5 @@
use crate::app::App;
use crate::challenges::{challenges_picker, HighScore};
use crate::challenges::{challenges_picker, Challenge, HighScore};
use crate::common::{ContextualActions, Tab};
use crate::cutscene::CutsceneBuilder;
use crate::edit::EditMode;
@ -19,11 +19,10 @@ use std::collections::BTreeMap;
// TODO A nice level to unlock: specifying your own commute, getting to work on it
const GOAL: Duration = Duration::const_seconds(3.0 * 60.0);
pub struct OptimizeCommute {
top_center: Composite,
person: PersonID,
goal: Duration,
time: Time,
// Cache here for convenience
@ -33,18 +32,32 @@ pub struct OptimizeCommute {
}
impl OptimizeCommute {
pub fn new(ctx: &mut EventCtx, app: &App, person: PersonID) -> Box<dyn GameplayState> {
pub fn new(
ctx: &mut EventCtx,
app: &App,
person: PersonID,
goal: Duration,
) -> Box<dyn GameplayState> {
let trips = app.primary.sim.get_person(person).trips.clone();
Box::new(OptimizeCommute {
top_center: make_top_center(ctx, app, Duration::ZERO, Duration::ZERO, 0, trips.len()),
top_center: make_top_center(
ctx,
app,
Duration::ZERO,
Duration::ZERO,
0,
trips.len(),
goal,
),
person,
goal,
time: Time::START_OF_DAY,
trips,
once: true,
})
}
pub fn cutscene(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
pub fn cutscene_pt1(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
CutsceneBuilder::new()
.scene("boss", "Listen up, I've got a special job for you today.")
.scene(
@ -70,7 +83,7 @@ impl OptimizeCommute {
.scene(
"boss",
"That's none of your concern! I've anonymized their name, so don't even bother \
digging into what happened at dinn --",
digging into what happened in Ballard --",
)
.scene("boss", "JUST GET TO WORK, KID!")
.narrator(
@ -83,7 +96,40 @@ impl OptimizeCommute {
)
.narrator(
"Ignore the damage done to everyone else. Just speed up the VIP's trips by a \
total of 3 minutes.",
total of 2 minutes.",
)
.build(ctx, app)
}
pub fn cutscene_pt2(ctx: &mut EventCtx, app: &App) -> Box<dyn State> {
CutsceneBuilder::new()
.scene(
"boss",
"I've got another, er, friend who's sick of this parking situation.",
)
.scene(
"player",
"Yeah, why do we dedicate so much valuable land to storing unused cars? It's \
ridiculous!",
)
.scene(
"boss",
"No, I mean, they're tired of having to hunt for parking. You need to make it \
easier.",
)
.scene(
"player",
"What? We're trying to encourage people to be less car-dependent. Why's this \
\"friend\" more important than the city's carbon-neutral goals?",
)
.scene(
"boss",
"Everyone's calling in favors these days. Just make it happen!",
)
.narrator("Too many people have dirt on the boss. Guess we have another VIP to help.")
.narrator(
"Once again, ignore the damage to everyone else, and just speed up the VIP's \
trips by a total of 5 minutes.",
)
.build(ctx, app)
}
@ -112,16 +158,18 @@ impl GameplayState for OptimizeCommute {
self.time = app.primary.sim.time();
let (before, after, done) = get_score(app, &self.trips);
self.top_center = make_top_center(ctx, app, before, after, done, self.trips.len());
self.top_center =
make_top_center(ctx, app, before, after, done, self.trips.len(), self.goal);
if done == self.trips.len() {
return (
Some(final_score(
ctx,
app,
GameplayMode::OptimizeCommute(self.person),
GameplayMode::OptimizeCommute(self.person, self.goal),
before,
after,
self.goal,
)),
false,
);
@ -135,14 +183,21 @@ impl GameplayState for OptimizeCommute {
Some(Transition::Push(Box::new(EditMode::new(
ctx,
app,
GameplayMode::OptimizeCommute(self.person),
GameplayMode::OptimizeCommute(self.person, self.goal),
)))),
false,
);
}
"instructions" => {
return (
Some(Transition::Push(OptimizeCommute::cutscene(ctx, app))),
Some(Transition::Push((Challenge::find(
&GameplayMode::OptimizeCommute(self.person, self.goal),
)
.0
.cutscene
.unwrap())(
ctx, app
))),
false,
);
}
@ -192,11 +247,12 @@ fn make_top_center(
after: Duration,
done: usize,
trips: usize,
goal: Duration,
) -> Composite {
let mut txt = Text::from(Line(format!("Total trip time: {} (", after)));
txt.append_all(cmp_duration_shorter(after, before));
txt.append(Line(")"));
let sentiment = if before - after >= GOAL {
let sentiment = if before - after >= goal {
"../data/system/assets/tools/happy.svg"
} else {
"../data/system/assets/tools/sad.svg"
@ -213,7 +269,7 @@ fn make_top_center(
.draw_text(ctx)
.margin_right(20),
txt.draw(ctx).margin_right(20),
format!("Goal: {} faster", GOAL)
format!("Goal: {} faster", goal)
.draw_text(ctx)
.margin_right(5),
Widget::draw_svg(ctx, sentiment).centered_vert(),
@ -232,7 +288,10 @@ fn final_score(
mode: GameplayMode,
before: Duration,
after: Duration,
goal: Duration,
) -> Transition {
let mut next_mode: Option<GameplayMode> = None;
let msg = if before == after {
format!(
"The VIP's commute still takes a total of {}. Were you asleep on the job? Try \
@ -246,14 +305,14 @@ fn final_score(
me over?!",
before, after
)
} else if before - after < GOAL {
} else if before - after < goal {
format!(
"The VIP's commute went from {} total to {}. Hmm... that's {} faster. But didn't I \
tell you to speed things up by {} at least?",
before,
after,
before - after,
GOAL
goal
)
} else {
// Blindly record the high school
@ -266,13 +325,15 @@ fn final_score(
.entry(mode.clone())
.or_insert_with(Vec::new);
scores.push(HighScore {
goal: GOAL,
goal,
score: before - after,
edits_name: app.primary.map.get_edits().edits_name.clone(),
});
scores.sort_by_key(|s| s.score);
scores.reverse();
next_mode = Challenge::find(&mode).1.map(|c| c.gameplay);
format!(
"Alright, you somehow managed to shave {} down from the VIP's original commute of {}. \
I guess that'll do. Maybe you're not totally useless after all.",
@ -292,8 +353,13 @@ fn final_score(
.padding(10),
Widget::col(vec![
msg.draw_text(ctx),
// TODO Adjust wording, optional continue option
// TODO Adjust wording
Btn::text_bg2("Try again").build_def(ctx, None),
if next_mode.is_some() {
Btn::text_bg2("Next challenge").build_def(ctx, None)
} else {
Widget::nothing()
},
Btn::text_bg2("Back to challenges").build_def(ctx, None),
])
.outline(10.0, Color::BLACK)
@ -303,6 +369,7 @@ fn final_score(
)
.build(ctx),
retry: mode,
next_mode,
}))
}
@ -333,6 +400,7 @@ impl ContextualActions for Actions {
struct FinalScore {
composite: Composite,
retry: GameplayMode,
next_mode: Option<GameplayMode>,
}
impl State for FinalScore {
@ -342,6 +410,14 @@ impl State for FinalScore {
"Try again" => {
Transition::Replace(Box::new(SandboxMode::new(ctx, app, self.retry.clone())))
}
"Next challenge" => Transition::Clear(vec![
main_menu(ctx, app),
Box::new(SandboxMode::new(ctx, app, self.next_mode.clone().unwrap())),
(Challenge::find(self.next_mode.as_ref().unwrap())
.0
.cutscene
.unwrap())(ctx, app),
]),
"Back to challenges" => {
Transition::Clear(vec![main_menu(ctx, app), challenges_picker(ctx, app)])
}

View File

@ -9,8 +9,7 @@ mod tutorial;
pub use self::tutorial::{Tutorial, TutorialPointer, TutorialState};
use crate::app::App;
use crate::challenges;
use crate::challenges::challenges_picker;
use crate::challenges::{challenges_picker, Challenge};
use crate::common::{CommonState, ContextualActions};
use crate::edit::EditMode;
use crate::game::{msg, State, Transition};
@ -28,7 +27,7 @@ use map_model::{EditCmd, EditIntersection, Map, MapEdits};
use rand_xorshift::XorShiftRng;
use sim::{Analytics, PersonID, Scenario, ScenarioGenerator};
#[derive(PartialEq, Eq, Hash, Clone)]
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
pub enum GameplayMode {
// TODO Maybe this should be "sandbox"
// Map path
@ -40,7 +39,7 @@ pub enum GameplayMode {
FixTrafficSignals,
// TODO Kinda gross. What stage in the tutorial?
FixTrafficSignalsTutorial(usize),
OptimizeCommute(PersonID),
OptimizeCommute(PersonID, Duration),
// current
Tutorial(TutorialPointer),
@ -93,7 +92,7 @@ impl GameplayMode {
GameplayMode::FixTrafficSignalsTutorial(_) => {
abstutil::path_synthetic_map("signal_single")
}
GameplayMode::OptimizeCommute(_) => abstutil::path_map("montlake"),
GameplayMode::OptimizeCommute(_, _) => abstutil::path_map("montlake"),
GameplayMode::Tutorial(_) => abstutil::path_map("montlake"),
}
}
@ -252,7 +251,9 @@ impl GameplayMode {
GameplayMode::FixTrafficSignals | GameplayMode::FixTrafficSignalsTutorial(_) => {
fix_traffic_signals::FixTrafficSignals::new(ctx, app, self.clone())
}
GameplayMode::OptimizeCommute(p) => commute::OptimizeCommute::new(ctx, app, *p),
GameplayMode::OptimizeCommute(p, goal) => {
commute::OptimizeCommute::new(ctx, app, *p, *goal)
}
GameplayMode::Tutorial(current) => Tutorial::new(ctx, app, *current),
}
}
@ -317,16 +318,7 @@ fn challenge_controller(
title: &str,
extra_rows: Vec<Widget>,
) -> WrappedComposite {
// Scrape the description
let mut description = Vec::new();
'OUTER: for (_, stages) in challenges::all_challenges(true) {
for challenge in stages {
if challenge.gameplay == gameplay {
description = challenge.description.clone();
break 'OUTER;
}
}
}
let description = Challenge::find(&gameplay).0.description;
let mut rows = vec![challenge_header(ctx, title)];
rows.extend(extra_rows);

View File

@ -31,7 +31,7 @@ pub struct Tutorial {
warped: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct TutorialPointer {
pub stage: usize,
// Index into messages. messages.len() means the actual task.