1use super::{Goal, Level, LevelKind};
9use crate::sim::board::{Board, CapPolicy, TileKind};
10use crate::sim::crab::{CrabKind, Handedness};
11use crate::sim::direction::Direction;
12use crate::sim::solve::Placement;
13use crate::sim::{PlayerId, Spawner};
14
15impl Level {
16 pub fn parse(text: &str) -> Result<Level, String> {
17 let mut lines = text.lines();
18 let header = parse_header(&mut lines)?;
19 let mut board = parse_lattice(lines, header.seed)?;
20 place_entities(&mut board, &header)?;
21 let level = Level {
22 name: header.name.ok_or("missing name:")?,
23 posts: header.posts.ok_or("missing posts:")?,
24 solution: header.solution,
25 goal: header.goal,
26 kind: LevelKind::Puzzle,
29 crab_count: header.crabs.len() as u32,
30 explicit_rule: header.rule.is_some(),
31 board,
32 };
33 let kind = header.kind.unwrap_or_else(|| inferred_kind(&level));
34 Ok(level.with_kind(kind))
35 }
36
37 pub fn from_board(name: impl Into<String>, posts: u8, board: Board) -> Level {
42 let level = Level {
43 name: name.into(),
44 posts,
45 solution: Vec::new(),
46 goal: Goal::AllCrabs,
47 kind: LevelKind::Puzzle,
48 crab_count: board.crabs().len() as u32,
49 explicit_rule: true,
51 board,
52 };
53 let kind = inferred_kind(&level);
54 level.with_kind(kind)
55 }
56
57 pub fn to_text(&self) -> String {
61 use std::fmt::Write;
62 let board = &self.board;
63 let mut out = String::new();
64 let _ = writeln!(out, "name: {}", self.name);
65 let _ = writeln!(out, "posts: {}", self.posts);
66 let _ = writeln!(out, "kind: {}", self.kind.token());
70 let _ = writeln!(out, "seed: {}", board.seed());
71 if self.explicit_rule {
74 let (cap, policy) = board.signpost_rule();
75 let _ = writeln!(out, "rule: {} {cap}", policy.token());
76 }
77 for (player, &score) in board.scores().iter().enumerate() {
78 if score > 0 {
79 let _ = writeln!(out, "score: {player} {score}");
80 }
81 }
82 for crab in board.crabs() {
83 let (x, y) = board.coords_u8(crab.tile);
84 let _ = writeln!(
85 out,
86 "crab: {x},{y} {} {} {}",
87 crab.dir.letter(),
88 crab.handed.token(),
89 crab.kind.token()
90 );
91 }
92 for (x, y, kind) in board.tiles() {
93 if let TileKind::Spawner(s) = kind {
94 let _ = writeln!(out, "spawner: {x},{y} {} {}", s.dir.letter(), s.period);
95 }
96 }
97 for gull in board.gulls() {
98 let (x, y) = board.coords_u8(gull.tile);
99 let _ = writeln!(out, "gull: {x},{y} {}", gull.dir.letter());
100 }
101 if board.gull_period() > 0 {
102 let _ = writeln!(out, "gull_period: {}", board.gull_period());
103 }
104 if let Some(round) = board.round_length() {
105 let _ = writeln!(out, "round: {round}");
106 }
107 if board.wrap() {
108 let _ = writeln!(out, "wrap: on");
109 }
110 if board.events_enabled() {
113 let _ = writeln!(out, "events: on");
114 }
115 match self.goal {
116 Goal::AllCrabs => {}
117 Goal::Bank(n) => {
118 let _ = writeln!(out, "goal: bank {n}");
119 }
120 Goal::Survive => {
121 let _ = writeln!(out, "goal: survive");
122 }
123 Goal::Golden => {
124 let _ = writeln!(out, "goal: golden");
125 }
126 }
127 if !self.solution.is_empty() {
128 let parts: Vec<String> = self
129 .solution
130 .iter()
131 .map(|&(x, y, dir)| format!("{x},{y} {}", dir.letter()))
132 .collect();
133 let _ = writeln!(out, "solution: {}", parts.join("; "));
134 }
135 let _ = writeln!(out, "map:");
136
137 let (w, h) = (board.width() as usize, board.height() as usize);
139 let mut lattice = vec![vec![' '; 2 * w + 1]; 2 * h + 1];
140 for row in lattice.iter_mut().step_by(2) {
141 for cell in row.iter_mut().step_by(2) {
142 *cell = '+';
143 }
144 }
145 for (x, y, kind) in board.tiles() {
146 let (lx, ly) = (2 * x as usize + 1, 2 * y as usize + 1);
147 lattice[ly][lx] = tile_glyph(kind);
148 if board.wall_at(x, y, Direction::Up) {
149 lattice[ly - 1][lx] = '-';
150 }
151 if board.wall_at(x, y, Direction::Left) {
152 lattice[ly][lx - 1] = '|';
153 }
154 if board.wall_at(x, y, Direction::Down) {
155 lattice[ly + 1][lx] = '-';
156 }
157 if board.wall_at(x, y, Direction::Right) {
158 lattice[ly][lx + 1] = '|';
159 }
160 }
161 for row in lattice {
162 let line: String = row.into_iter().collect();
163 let _ = writeln!(out, "{}", line.trim_end());
164 }
165 out
166 }
167}
168
169fn inferred_kind(level: &Level) -> LevelKind {
175 match level.seats() >= 2 {
176 true => LevelKind::Arena,
177 false => LevelKind::Puzzle,
178 }
179}
180
181fn parse_xy(s: &str) -> Result<(u8, u8, &str), String> {
182 let (xy, rest) = match s.split_once(char::is_whitespace) {
183 Some((xy, rest)) => (xy, rest),
184 None => (s, ""),
185 };
186 let (x, y) = xy
187 .split_once(',')
188 .ok_or_else(|| format!("expected x,y in {s:?}"))?;
189 let x = x.trim().parse::<u8>().map_err(|e| format!("x: {e}"))?;
190 let y = y.trim().parse::<u8>().map_err(|e| format!("y: {e}"))?;
191 Ok((x, y, rest))
192}
193
194struct Header {
197 name: Option<String>,
198 posts: Option<u8>,
199 solution: Vec<Placement>,
200 crabs: Vec<(u8, u8, Direction, Handedness, CrabKind)>,
201 spawners: Vec<(u8, u8, Direction, u32)>,
202 gulls: Vec<(u8, u8, Direction)>,
203 gull_period: u32,
204 events: bool,
205 round: Option<u32>,
206 seed: u64,
207 rule: Option<(u8, CapPolicy)>,
208 scores: Vec<(u8, u32)>,
209 goal: Goal,
210 wrap: bool,
211 kind: Option<LevelKind>,
213}
214
215impl Default for Header {
216 fn default() -> Header {
217 Header {
218 name: None,
219 posts: None,
220 solution: Vec::new(),
221 crabs: Vec::new(),
222 spawners: Vec::new(),
223 gulls: Vec::new(),
224 gull_period: 0,
225 events: false,
226 round: None,
227 seed: 0x7ead_0001,
230 rule: None,
231 scores: Vec::new(),
232 goal: Goal::AllCrabs,
233 wrap: false,
234 kind: None,
235 }
236 }
237}
238
239enum HeaderLine {
241 Read,
242 MapFollows,
244}
245
246impl Header {
247 fn read_line(&mut self, key: &str, value: &str) -> Result<HeaderLine, String> {
251 match key {
252 "name" => self.name = Some(value.to_string()),
253 "posts" => {
254 self.posts = Some(value.parse::<u8>().map_err(|e| format!("posts: {e}"))?);
255 }
256 "crab" => {
257 let (x, y, rest) = parse_xy(value)?;
258 let mut parts = rest.split_whitespace();
259 let dir = parse_dir(parts.next().ok_or("crab: missing direction")?)?;
260 let handed = parts.next().ok_or("crab: missing handedness")?;
261 let handed = Handedness::from_token(handed)
262 .ok_or_else(|| format!("crab: bad handedness {handed:?}"))?;
263 let kind = parts.next().ok_or("crab: missing kind")?;
264 let kind =
265 CrabKind::from_token(kind).ok_or_else(|| format!("crab: bad kind {kind:?}"))?;
266 self.crabs.push((x, y, dir, handed, kind));
267 }
268 "spawner" => {
269 let (x, y, rest) = parse_xy(value)?;
270 let mut parts = rest.split_whitespace();
271 let dir = parse_dir(parts.next().ok_or("spawner: missing direction")?)?;
272 let period = parts
273 .next()
274 .ok_or("spawner: missing period")?
275 .parse::<u32>()
276 .map_err(|e| format!("spawner period: {e}"))?;
277 self.spawners.push((x, y, dir, period));
278 }
279 "gull" => {
280 let (x, y, rest) = parse_xy(value)?;
281 let dir = parse_dir(rest.trim())?;
282 self.gulls.push((x, y, dir));
283 }
284 "gull_period" => {
285 self.gull_period = value
286 .parse::<u32>()
287 .map_err(|e| format!("gull_period: {e}"))?;
288 }
289 "round" => {
290 self.round = Some(value.parse::<u32>().map_err(|e| format!("round: {e}"))?);
291 }
292 "seed" => {
293 self.seed = value.parse::<u64>().map_err(|e| format!("seed: {e}"))?;
294 }
295 "goal" => {
296 self.goal = match value.split_once(' ') {
297 Some(("bank", n)) => {
298 Goal::Bank(n.trim().parse().map_err(|e| format!("goal bank: {e}"))?)
299 }
300 None if value == "survive" => Goal::Survive,
301 None if value == "golden" => Goal::Golden,
302 None if value == "all" => Goal::AllCrabs,
303 _ => return Err(format!("goal: bad value {value:?}")),
304 };
305 }
306 "kind" => {
307 self.kind = Some(
308 LevelKind::from_token(value)
309 .ok_or_else(|| format!("kind: bad value {value:?}"))?,
310 );
311 }
312 "wrap" => self.wrap = value == "on",
313 "events" => self.events = value == "on",
314 "rule" => {
315 let (policy, cap) = value
316 .split_once(' ')
317 .ok_or("rule: expected `<evict|reject> <cap>`")?;
318 let cap = cap
319 .trim()
320 .parse::<u8>()
321 .map_err(|e| format!("rule cap: {e}"))?;
322 let policy = policy.trim();
323 let policy = CapPolicy::from_token(policy)
324 .ok_or_else(|| format!("rule: bad policy {policy:?}"))?;
325 self.rule = Some((cap, policy));
326 }
327 "score" => {
328 let (player, score) = value
329 .split_once(' ')
330 .ok_or("score: expected `<player> <score>`")?;
331 let player = player
332 .trim()
333 .parse::<u8>()
334 .map_err(|e| format!("score: {e}"))?;
335 let score = score
336 .trim()
337 .parse::<u32>()
338 .map_err(|e| format!("score: {e}"))?;
339 if crate::sim::board::seat(player).is_none() {
340 return Err(format!("score: player {player} out of range"));
341 }
342 self.scores.push((player, score));
343 }
344 "solution" => {
345 for placement in value.split(';') {
346 let (x, y, rest) = parse_xy(placement.trim())?;
347 let dir = parse_dir(rest.trim())?;
348 self.solution.push((x, y, dir));
349 }
350 }
351 "map" => return Ok(HeaderLine::MapFollows),
352 other => return Err(format!("unknown key {other:?}")),
353 }
354 Ok(HeaderLine::Read)
355 }
356}
357
358fn parse_header(lines: &mut std::str::Lines) -> Result<Header, String> {
360 let mut header = Header::default();
361 for line in lines.by_ref() {
362 let line = line.trim();
363 if line.is_empty() || line.starts_with('#') {
365 continue;
366 }
367 let Some((key, value)) = line.split_once(':') else {
368 return Err(format!("expected `key: value` before map, got {line:?}"));
369 };
370 if let HeaderLine::MapFollows = header.read_line(key.trim(), value.trim())? {
371 break;
372 }
373 }
374 Ok(header)
375}
376
377fn parse_lattice<'a>(lines: impl Iterator<Item = &'a str>, seed: u64) -> Result<Board, String> {
379 let lattice: Vec<&str> = lines.filter(|l: &&str| !l.trim().is_empty()).collect();
380 if lattice.is_empty() {
381 return Err("missing map section".into());
382 }
383 let lat_h = lattice.len();
384 let lat_w = lattice[0].chars().count();
385 if lat_h.is_multiple_of(2) || lat_w.is_multiple_of(2) {
386 return Err(format!(
387 "map lattice must be odd-sized, got {lat_w}×{lat_h}"
388 ));
389 }
390 if lat_w < 3 || lat_h < 3 {
397 return Err(format!(
398 "map lattice must hold at least one tile, got {lat_w}×{lat_h}"
399 ));
400 }
401 let (tiles_w, tiles_h) = (lat_w / 2, lat_h / 2);
402 let (Ok(width), Ok(height)) = (u8::try_from(tiles_w), u8::try_from(tiles_h)) else {
403 return Err(format!(
404 "map is {tiles_w}×{tiles_h} tiles, past the {} a side the format can name",
405 u8::MAX
406 ));
407 };
408 let grid: Vec<Vec<char>> = lattice
412 .iter()
413 .map(|row| {
414 let mut chars: Vec<char> = row.chars().collect();
415 chars.resize(lat_w, ' ');
416 chars
417 })
418 .collect();
419
420 debug_assert!(width > 0 && height > 0, "a {width}x{height} board");
426 let mut board = Board::new(width, height, seed);
427 for y in 0..height {
428 for x in 0..width {
429 let c = grid[y as usize * 2 + 1][x as usize * 2 + 1];
430 match tile_from_glyph(c) {
431 Some(TileKind::Empty) => {}
432 Some(tile) => board.set_tile(x, y, tile),
433 None => return Err(format!("bad tile char {c:?} at ({x},{y})")),
434 }
435 if grid[y as usize * 2][x as usize * 2 + 1] == '-' {
437 board.set_wall(x, y, Direction::Up, true);
438 }
439 if grid[y as usize * 2 + 1][x as usize * 2] == '|' {
441 board.set_wall(x, y, Direction::Left, true);
442 }
443 }
444 }
445 for x in 0..width {
447 if grid[height as usize * 2][x as usize * 2 + 1] == '-' {
448 board.set_wall(x, height - 1, Direction::Down, true);
449 }
450 }
451 for y in 0..height {
452 if grid[y as usize * 2 + 1][width as usize * 2] == '|' {
453 board.set_wall(width - 1, y, Direction::Right, true);
454 }
455 }
456 Ok(board)
457}
458
459fn place_entities(board: &mut Board, header: &Header) -> Result<(), String> {
462 let (width, height) = (board.width(), board.height());
463 let check = |what: &str, x: u8, y: u8| -> Result<(), String> {
464 if x >= width || y >= height {
465 return Err(format!(
466 "{what} at ({x},{y}) is off the {width}x{height} board"
467 ));
468 }
469 Ok(())
470 };
471 for &(x, y, dir, period) in &header.spawners {
472 check("spawner", x, y)?;
473 if period == 0 {
474 return Err(format!("spawner at ({x},{y}): period must be at least 1"));
475 }
476 board.set_tile(x, y, TileKind::Spawner(Spawner { dir, period }));
477 }
478 for &(x, y, dir, handed, kind) in &header.crabs {
479 check("crab", x, y)?;
480 if board.tile_at(x, y) == TileKind::Rock {
481 return Err(format!("crab at ({x},{y}) is standing on a rock"));
482 }
483 board.spawn_crab(x, y, dir, handed, kind);
484 }
485 for &(x, y, dir) in &header.gulls {
486 check("gull", x, y)?;
487 if board.tile_at(x, y) == TileKind::Rock {
488 return Err(format!("gull at ({x},{y}) is standing on a rock"));
489 }
490 board.spawn_gull(x, y, dir);
491 }
492 board.set_gull_period(header.gull_period);
493 board.set_round_length(header.round);
494 if header.wrap {
495 board.set_wrap(true);
496 }
497 if header.events {
498 board.set_events_enabled(true);
499 }
500 if let Some((cap, policy)) = header.rule {
501 board.set_signpost_rule(cap, policy);
502 }
503 for &(player, score) in &header.scores {
504 board.set_score(player, score);
505 }
506 Ok(())
507}
508
509fn tile_glyph(tile: TileKind) -> char {
512 match tile {
513 TileKind::Empty | TileKind::Spawner(_) => '.',
514 TileKind::Rock => '#',
515 TileKind::Castle(owner) => (b'0' + owner) as char,
516 TileKind::Turnstile { next_right: true } => 'T',
521 TileKind::Turnstile { next_right: false } => 't',
522 TileKind::Kelp => 'K',
523 TileKind::Pool => '~',
524 }
525}
526
527fn tile_from_glyph(c: char) -> Option<TileKind> {
528 match c {
529 '.' | ' ' => Some(TileKind::Empty),
530 '#' => Some(TileKind::Rock),
531 'T' => Some(TileKind::Turnstile { next_right: true }),
532 't' => Some(TileKind::Turnstile { next_right: false }),
533 'K' => Some(TileKind::Kelp),
534 '~' => Some(TileKind::Pool),
535 '0'..='9' if crate::sim::board::seat(c as u8 - b'0').is_some() => {
539 Some(TileKind::Castle((c as u8 - b'0') as PlayerId))
540 }
541 _ => None,
542 }
543}
544
545fn parse_dir(s: &str) -> Result<Direction, String> {
546 Direction::from_letter(s).ok_or_else(|| format!("bad direction {s:?}"))
547}