pinch_points/app/
campaign.rs1use crate::app::editor;
5use crate::sim::{Level, LevelKind};
6use bevy::prelude::*;
7
8#[derive(Clone, Copy, PartialEq, Eq, Debug)]
10pub enum CampaignKind {
11 TidePool,
12 BeachDay,
13}
14
15impl CampaignKind {
16 pub fn key(self) -> &'static str {
19 match self {
20 CampaignKind::TidePool => "tide",
21 CampaignKind::BeachDay => "beach",
22 }
23 }
24}
25
26#[derive(Resource)]
27pub struct Campaign {
28 pub kind: CampaignKind,
29 pub levels: Vec<Level>,
30 pub index: usize,
31 pub builtins: usize,
34}
35
36impl Campaign {
37 pub fn current(&self) -> &Level {
38 &self.levels[self.index]
39 }
40
41 pub(crate) fn reset(&mut self, kind: CampaignKind, levels: Vec<Level>, builtins: usize) {
44 let builtins = builtins.min(levels.len());
45 *self = Campaign {
46 kind,
47 levels,
48 index: 0,
49 builtins,
50 };
51 }
52}
53
54pub(crate) fn tide_pool_levels() -> (Vec<Level>, usize) {
57 let mut levels = crate::sim::campaign_levels();
58 let builtins = levels.len();
59 levels.extend(custom_puzzles(load_custom_levels()));
60 disambiguate(&mut levels, builtins);
61 (levels, builtins)
62}
63
64pub(crate) fn disambiguate(levels: &mut [Level], builtins: usize) {
72 let mut taken: std::collections::HashSet<String> = levels[..builtins.min(levels.len())]
73 .iter()
74 .map(|level| level.name.clone())
75 .collect();
76 for level in levels.iter_mut().skip(builtins) {
77 if taken.insert(level.name.clone()) {
78 continue;
79 }
80 let renamed = (2..)
81 .map(|n| format!("{} ({n})", level.name))
82 .find(|candidate| !taken.contains(candidate))
83 .unwrap_or_else(|| level.name.clone());
84 taken.insert(renamed.clone());
85 level.name = renamed;
86 }
87}
88
89pub(crate) fn custom_puzzles(levels: Vec<Level>) -> Vec<Level> {
94 levels
95 .into_iter()
96 .filter(|level| level.kind == LevelKind::Puzzle && level.crab_count() > 0)
97 .collect()
98}
99
100pub(crate) fn custom_arenas(levels: Vec<Level>) -> Vec<Level> {
104 levels
105 .into_iter()
106 .filter(|level| level.kind == LevelKind::Arena)
107 .collect()
108}
109
110pub(crate) fn load_custom_levels() -> Vec<Level> {
115 let mut paths = vec![editor::legacy_save_path()];
116 if let Ok(dir) = std::fs::read_dir(editor::custom_dir()) {
117 let mut extra: Vec<_> = dir
118 .filter_map(Result::ok)
119 .map(|e| e.path())
120 .filter(|p| p.extension().is_some_and(|ext| ext == "txt"))
121 .collect();
122 extra.sort();
123 paths.extend(extra);
124 }
125 let mut levels = Vec::new();
126 for path in paths {
127 let Ok(text) = std::fs::read_to_string(&path) else {
128 continue; };
130 match Level::parse(&text) {
131 Ok(level) => {
132 info!(
133 "loaded custom {} {:?} from {}",
134 level.kind.token(),
135 level.name,
136 path.display()
137 );
138 if level.kind == LevelKind::Puzzle && level.crab_count() == 0 {
142 warn!("{}: a puzzle with no crabs to route", path.display());
143 }
144 if level.kind == LevelKind::Arena && level.seats() < 2 {
145 warn!(
146 "{}: a beach with castles for {} - no table can sit at it",
147 path.display(),
148 level.seats()
149 );
150 }
151 levels.push(level);
152 }
153 Err(e) => warn!("skipping {}: {e}", path.display()),
154 }
155 }
156 levels
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn level(name: &str, kind: &str, crabs: bool) -> Level {
164 let crab = if crabs { "crab: 0,0 R L common\n" } else { "" };
165 Level::parse(&format!(
166 "name: {name}\nposts: 2\nkind: {kind}\n{crab}map:\n+-+-+-+\n|0 . 1|\n+-+-+-+\n"
167 ))
168 .expect("a level")
169 }
170
171 #[test]
174 fn the_shelf_splits_by_kind() {
175 let shelf = || {
176 vec![
177 level("Stage", "puzzle", true),
178 level("Beach", "arena", true),
179 level("Empty Beach", "arena", false),
180 level("Crabless", "puzzle", false),
181 ]
182 };
183 let names = |levels: Vec<Level>| -> Vec<String> {
184 levels.into_iter().map(|level| level.name).collect()
185 };
186 assert_eq!(names(custom_puzzles(shelf())), ["Stage"]);
187 assert_eq!(names(custom_arenas(shelf())), ["Beach", "Empty Beach"]);
190 }
191
192 #[test]
195 fn shipped_level_names_are_unique() {
196 for levels in [
197 crate::sim::campaign_levels(),
198 crate::sim::challenge_levels(),
199 ] {
200 let mut seen = std::collections::HashSet::new();
201 for level in &levels {
202 assert!(seen.insert(level.name.clone()), "{:?} twice", level.name);
203 }
204 }
205 }
206
207 #[test]
211 fn same_named_player_levels_are_told_apart() {
212 let mut levels = vec![
213 level("Welcome Ashore", "puzzle", true),
214 level("Gull Alley", "puzzle", true),
215 level("Welcome Ashore", "puzzle", true),
216 level("Mine", "puzzle", true),
217 level("Mine", "puzzle", true),
218 level("Mine", "puzzle", true),
219 level("Mine (2)", "puzzle", true),
220 ];
221 disambiguate(&mut levels, 2);
222 let names: Vec<&str> = levels.iter().map(|level| level.name.as_str()).collect();
223 assert_eq!(
224 names,
225 [
226 "Welcome Ashore",
227 "Gull Alley",
228 "Welcome Ashore (2)",
229 "Mine",
230 "Mine (2)",
231 "Mine (3)",
232 "Mine (2) (2)",
233 ]
234 );
235 }
236}