-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathanalyze.rs
236 lines (203 loc) · 9.09 KB
/
analyze.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//! # "Interesting Facts" Source
//!
//! This example queries and computes the statistics found in the README.
//!
//! ## Usage
//!
//! `cargo run [--release] --example analyze -- PATH`
//!
//! - `PATH` is a path to a file containing previously generated outcome data.
use std::collections::HashMap;
use std::error::Error;
use std::fs::File;
use rmp_serde::Deserializer;
use serde::Deserialize;
use me2finale::prelude::*;
fn main() -> Result<(), Box<dyn Error>> {
let Some(path) = std::env::args().nth(1) else {
return Err("PATH argument is required".into());
};
println!("Loading outcome data...");
let file = File::open(&path)?;
let mut deserializer = Deserializer::new(file);
let outcome_map = OutcomeMap::deserialize(&mut deserializer)?;
println!();
let total_decision_paths = outcome_map
.values()
.map(|metadata| metadata.count)
.sum::<usize>();
println!("There are {total_decision_paths} total decision paths.");
let total_outcomes = outcome_map.len();
println!("There are {total_outcomes} total outcomes.");
println!();
// Which outcomes can only be achieved with one specific decision path?
{
let unique_outcomes = outcome_map
.iter()
.filter_map(|(outcome, metadata)| (metadata.count == 1).then_some(outcome))
.collect::<Vec<_>>();
let unique_outcome_count = unique_outcomes.len();
println!("{unique_outcome_count} outcomes are uniquely achievable.");
// Are there any commonalities between the unique outcomes?
let mut common_survivors = AllySet::EVERYONE;
let mut common_victims = AllySet::EVERYONE;
let mut common_loyal = AllySet::EVERYONE;
let mut common_disloyal = AllySet::EVERYONE;
let mut common_crew: Result<bool, bool> = Err(true);
for outcome in unique_outcomes {
common_survivors &= outcome.survivors;
common_victims -= outcome.survivors;
common_loyal &= outcome.loyal;
common_disloyal -= outcome.loyal;
match common_crew {
Ok(crew) => {
if crew != outcome.rescued_crew {
common_crew = Err(false);
}
}
Err(true) => common_crew = Ok(outcome.rescued_crew),
Err(false) => {}
}
}
println!("The unique outcomes have the following commonalities:");
println!("- Survivors: {common_survivors}");
println!("- Victims: {common_victims}");
println!("- Loyal: {common_loyal}");
println!("- Disloyal: {common_disloyal}");
if let Ok(common_crew) = common_crew {
let verb = if common_crew { "Rescued" } else { "Abandoned" };
println!("- Crew: {verb}");
}
}
println!();
// What is the outcome achievable by the most decision paths?
{
let (most_common_outcome, max_metadata) = outcome_map
.iter()
.max_by_key(|(_, metadata)| metadata.count)
.expect("`outcome_map` is not empty");
let max_decision_paths = max_metadata.count;
println!("The most common outcome can be achieved in {max_decision_paths} ways:");
println!("{most_common_outcome}");
}
// How many decision paths lead to the top X outcomes?
{
let mut decision_path_counts = outcome_map
.values()
.map(|metadata| metadata.count)
.collect::<Vec<_>>();
decision_path_counts.sort();
let mut decision_path_counts = decision_path_counts.into_iter().rev();
let mut top_x_count = decision_path_counts.by_ref().take(10).sum::<usize>();
println!("The ten most common outcomes cover {top_x_count} decision paths.");
top_x_count += decision_path_counts.by_ref().take(90).sum::<usize>();
println!("The one hundred most common outcomes cover {top_x_count} decision paths.");
top_x_count += decision_path_counts.take(900).sum::<usize>();
println!("The one thousand most common outcomes cover {top_x_count} decision paths.");
}
println!();
// In how many ways can you fail the final mission (i.e., Shepard dies)?
{
let (failed_outcome_count, failed_decision_path_count) = outcome_map
.iter()
.filter_map(|(outcome, metadata)| {
(outcome.survivors.len() < 2).then_some(metadata.count)
})
.enumerate()
// This is a cheeky way to calculate a count and a sum with the same iterator.
.fold((0, 0), |(_, sum), (i, count)| (i, sum + count));
print!("There are {failed_outcome_count} outcomes in which Shepard dies, covering ");
println!("{failed_decision_path_count} decision paths.");
let shepard_survivals = total_decision_paths - failed_decision_path_count;
let shepard_survival_rate = shepard_survivals as f32 / total_decision_paths as f32;
let shepard_survival_percent = 100. * shepard_survival_rate;
println!("- Shepard survives {shepard_survival_percent:.2}% of all decision paths.");
}
println!();
// What does it take to achieve the "best" possible outcome?
{
// Morinth and Samara are mutually exclusive, and replacing Samara with Morinth leads to
// negative consequences in Mass Effect 3. Therefore, the "best" outcome excludes Morinth.
let best_outcome = Outcome {
survivors: !Ally::Morinth,
loyal: !Ally::Morinth,
rescued_crew: true,
};
println!("The best possible outcome is defined as:\n{best_outcome}");
if let Some(metadata) = outcome_map.get(&best_outcome) {
println!(
"There are {} ways to achieve the best possible outcome.",
metadata.count
);
println!("Here is one example:");
println!();
print!("{}", metadata.example);
} else {
println!("There is no way to achieve the best possible outcome. :(");
}
}
println!();
// Create a table of absolute ally survival rates.
{
#[derive(Default)]
struct SurvivalCounts {
loyal: usize,
disloyal: usize,
}
struct SurvivalRates {
loyal: f32,
disloyal: f32,
total: f32,
}
let mut survival_counts: HashMap<_, SurvivalCounts> =
HashMap::with_capacity(AllySet::EVERYONE.len());
for (outcome, metadata) in outcome_map.iter() {
for ally in outcome.survivors {
match outcome.loyal.contains(ally) {
true => survival_counts.entry(ally).or_default().loyal += metadata.count,
false => survival_counts.entry(ally).or_default().disloyal += metadata.count,
}
}
}
let survival_counts = survival_counts;
// Sort the table rows by descending total survival rate.
let table_row_order = {
let mut ally_and_total_survival = survival_counts
.iter()
.map(|(&ally, counts)| (ally, counts.loyal + counts.disloyal))
.collect::<Vec<_>>();
ally_and_total_survival.sort_by_key(|&(_, total)| total);
ally_and_total_survival
.into_iter()
.rev()
.map(|(ally, _)| ally)
.collect::<Vec<_>>()
};
let absolute_survival_rates = survival_counts
.into_iter()
.map(|(ally, counts)| {
let rates = SurvivalRates {
loyal: counts.loyal as f32 / total_decision_paths as f32,
disloyal: counts.disloyal as f32 / total_decision_paths as f32,
total: (counts.loyal + counts.disloyal) as f32 / total_decision_paths as f32,
};
(ally, rates)
})
.collect::<HashMap<_, _>>();
println!("┌────────────────────────────────────────┐");
println!("│ Absolute Ally Survival Rates │");
println!("╞═════════╤═════════╤══════════╤═════════╡");
println!("│ Ally │ Loyal │ Disloyal │ Total │");
println!("├─────────┼─────────┼──────────┼─────────┤");
for ally in table_row_order {
if let Some(rates) = absolute_survival_rates.get(&ally) {
print!("│ {:7} │ {:7.5} ", ally.name(), rates.loyal);
println!("│ {:7.5} │ {:7.5} │", rates.disloyal, rates.total);
} else {
println!("│ {:7} │ ─── │ ──── │ ─── │", ally.name());
}
}
println!("└─────────┴─────────┴──────────┴─────────┘");
}
Ok(())
}