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 {
67 use std::fmt::Write;
68 let board = &self.board;
69 let mut out = String::new();
70 let _ = writeln!(out, "name: {}", self.name);
71 let _ = writeln!(out, "posts: {}", self.posts);
72 let _ = writeln!(out, "kind: {}", self.kind.token());
76 let _ = writeln!(out, "seed: {}", board.seed());
77 if self.explicit_rule {
80 let (cap, policy) = board.signpost_rule();
81 let _ = writeln!(out, "rule: {} {cap}", policy.token());
82 }
83 for (player, &score) in board.scores().iter().enumerate() {
84 if score > 0 {
85 let _ = writeln!(out, "score: {player} {score}");
86 }
87 }
88 for crab in board.crabs() {
89 let (x, y) = board.coords_u8(crab.tile);
90 let _ = writeln!(
91 out,
92 "crab: {x},{y} {} {} {}",
93 crab.dir.letter(),
94 crab.handed.token(),
95 crab.kind.token()
96 );
97 }
98 for (x, y, kind) in board.tiles() {
99 if let TileKind::Spawner(s) = kind {
100 let _ = writeln!(out, "spawner: {x},{y} {} {}", s.dir.letter(), s.period);
101 }
102 }
103 for gull in board.gulls() {
104 let (x, y) = board.coords_u8(gull.tile);
105 let _ = writeln!(out, "gull: {x},{y} {}", gull.dir.letter());
106 }
107 if board.gull_period() > 0 {
108 let _ = writeln!(out, "gull_period: {}", board.gull_period());
109 }
110 if let Some(round) = board.round_length() {
111 let _ = writeln!(out, "round: {round}");
112 }
113 if board.wrap() {
114 let _ = writeln!(out, "wrap: on");
115 }
116 if !board.castle_raids() {
121 let _ = writeln!(out, "raids: off");
122 }
123 if board.events_enabled() {
126 let _ = writeln!(out, "events: on");
127 }
128 match self.goal {
129 Goal::AllCrabs => {}
130 Goal::Bank(n) => {
131 let _ = writeln!(out, "goal: bank {n}");
132 }
133 Goal::Survive => {
134 let _ = writeln!(out, "goal: survive");
135 }
136 Goal::Golden => {
137 let _ = writeln!(out, "goal: golden");
138 }
139 }
140 if !self.solution.is_empty() {
141 let parts: Vec<String> = self
142 .solution
143 .iter()
144 .map(|&(x, y, dir)| format!("{x},{y} {}", dir.letter()))
145 .collect();
146 let _ = writeln!(out, "solution: {}", parts.join("; "));
147 }
148 let _ = writeln!(out, "map:");
149
150 let (w, h) = (board.width() as usize, board.height() as usize);
152 let mut lattice = vec![vec![' '; 2 * w + 1]; 2 * h + 1];
153 for row in lattice.iter_mut().step_by(2) {
154 for cell in row.iter_mut().step_by(2) {
155 *cell = '+';
156 }
157 }
158 for (x, y, kind) in board.tiles() {
159 let (lx, ly) = (2 * x as usize + 1, 2 * y as usize + 1);
160 lattice[ly][lx] = tile_glyph(kind);
161 if board.wall_at(x, y, Direction::Up) {
162 lattice[ly - 1][lx] = '-';
163 }
164 if board.wall_at(x, y, Direction::Left) {
165 lattice[ly][lx - 1] = '|';
166 }
167 if board.wall_at(x, y, Direction::Down) {
168 lattice[ly + 1][lx] = '-';
169 }
170 if board.wall_at(x, y, Direction::Right) {
171 lattice[ly][lx + 1] = '|';
172 }
173 }
174 for row in lattice {
175 let line: String = row.into_iter().collect();
176 let _ = writeln!(out, "{}", line.trim_end());
177 }
178 out
179 }
180}
181
182fn inferred_kind(level: &Level) -> LevelKind {
188 match level.seats() >= 2 {
189 true => LevelKind::Arena,
190 false => LevelKind::Puzzle,
191 }
192}
193
194fn parse_xy(s: &str) -> Result<(u8, u8, &str), String> {
195 let (xy, rest) = match s.split_once(char::is_whitespace) {
196 Some((xy, rest)) => (xy, rest),
197 None => (s, ""),
198 };
199 let (x, y) = xy
200 .split_once(',')
201 .ok_or_else(|| format!("expected x,y in {s:?}"))?;
202 let x = x.trim().parse::<u8>().map_err(|e| format!("x: {e}"))?;
203 let y = y.trim().parse::<u8>().map_err(|e| format!("y: {e}"))?;
204 Ok((x, y, rest))
205}
206
207struct Header {
210 name: Option<String>,
211 posts: Option<u8>,
212 solution: Vec<Placement>,
213 crabs: Vec<(u8, u8, Direction, Handedness, CrabKind)>,
214 spawners: Vec<(u8, u8, Direction, u32)>,
215 gulls: Vec<(u8, u8, Direction)>,
216 gull_period: u32,
217 events: bool,
218 round: Option<u32>,
219 seed: u64,
220 rule: Option<(u8, CapPolicy)>,
221 scores: Vec<(u8, u32)>,
222 goal: Goal,
223 wrap: bool,
224 kind: Option<LevelKind>,
226 no_castle_raids: bool,
231}
232
233impl Default for Header {
234 fn default() -> Header {
235 Header {
236 name: None,
237 posts: None,
238 solution: Vec::new(),
239 crabs: Vec::new(),
240 spawners: Vec::new(),
241 gulls: Vec::new(),
242 gull_period: 0,
243 events: false,
244 round: None,
245 seed: 0x7ead_0001,
248 rule: None,
249 scores: Vec::new(),
250 goal: Goal::AllCrabs,
251 wrap: false,
252 kind: None,
253 no_castle_raids: false,
254 }
255 }
256}
257
258enum HeaderLine {
260 Read,
261 MapFollows,
263}
264
265impl Header {
266 fn read_line(&mut self, key: &str, value: &str) -> Result<HeaderLine, String> {
270 match key {
271 "name" => self.name = Some(value.to_string()),
272 "posts" => {
273 self.posts = Some(value.parse::<u8>().map_err(|e| format!("posts: {e}"))?);
274 }
275 "crab" => {
276 let (x, y, rest) = parse_xy(value)?;
277 let mut parts = rest.split_whitespace();
278 let dir = parse_dir(parts.next().ok_or("crab: missing direction")?)?;
279 let handed = parts.next().ok_or("crab: missing handedness")?;
280 let handed = Handedness::from_token(handed)
281 .ok_or_else(|| format!("crab: bad handedness {handed:?}"))?;
282 let kind = parts.next().ok_or("crab: missing kind")?;
283 let kind =
284 CrabKind::from_token(kind).ok_or_else(|| format!("crab: bad kind {kind:?}"))?;
285 self.crabs.push((x, y, dir, handed, kind));
286 }
287 "spawner" => {
288 let (x, y, rest) = parse_xy(value)?;
289 let mut parts = rest.split_whitespace();
290 let dir = parse_dir(parts.next().ok_or("spawner: missing direction")?)?;
291 let period = parts
292 .next()
293 .ok_or("spawner: missing period")?
294 .parse::<u32>()
295 .map_err(|e| format!("spawner period: {e}"))?;
296 self.spawners.push((x, y, dir, period));
297 }
298 "gull" => {
299 let (x, y, rest) = parse_xy(value)?;
300 let dir = parse_dir(rest.trim())?;
301 self.gulls.push((x, y, dir));
302 }
303 "gull_period" => {
304 self.gull_period = value
305 .parse::<u32>()
306 .map_err(|e| format!("gull_period: {e}"))?;
307 }
308 "round" => {
309 self.round = Some(value.parse::<u32>().map_err(|e| format!("round: {e}"))?);
310 }
311 "seed" => {
312 self.seed = value.parse::<u64>().map_err(|e| format!("seed: {e}"))?;
313 }
314 "goal" => {
315 self.goal = match value.split_once(' ') {
316 Some(("bank", n)) => {
317 Goal::Bank(n.trim().parse().map_err(|e| format!("goal bank: {e}"))?)
318 }
319 None if value == "survive" => Goal::Survive,
320 None if value == "golden" => Goal::Golden,
321 None if value == "all" => Goal::AllCrabs,
322 _ => return Err(format!("goal: bad value {value:?}")),
323 };
324 }
325 "raids" => self.no_castle_raids = value == "off",
326 "kind" => {
327 self.kind = Some(
328 LevelKind::from_token(value)
329 .ok_or_else(|| format!("kind: bad value {value:?}"))?,
330 );
331 }
332 "wrap" => self.wrap = value == "on",
333 "events" => self.events = value == "on",
334 "rule" => {
335 let (policy, cap) = value
336 .split_once(' ')
337 .ok_or("rule: expected `<evict|reject> <cap>`")?;
338 let cap = cap
339 .trim()
340 .parse::<u8>()
341 .map_err(|e| format!("rule cap: {e}"))?;
342 let policy = policy.trim();
343 let policy = CapPolicy::from_token(policy)
344 .ok_or_else(|| format!("rule: bad policy {policy:?}"))?;
345 if cap == 0 && policy == CapPolicy::Evict {
349 return Err("rule: evict needs a cap of at least 1".to_string());
350 }
351 self.rule = Some((cap, policy));
352 }
353 "score" => {
354 let (player, score) = value
355 .split_once(' ')
356 .ok_or("score: expected `<player> <score>`")?;
357 let player = player
358 .trim()
359 .parse::<u8>()
360 .map_err(|e| format!("score: {e}"))?;
361 let score = score
362 .trim()
363 .parse::<u32>()
364 .map_err(|e| format!("score: {e}"))?;
365 if crate::sim::board::seat(player).is_none() {
366 return Err(format!("score: player {player} out of range"));
367 }
368 self.scores.push((player, score));
369 }
370 "solution" => {
371 for placement in value.split(';') {
372 let (x, y, rest) = parse_xy(placement.trim())?;
373 let dir = parse_dir(rest.trim())?;
374 self.solution.push((x, y, dir));
375 }
376 }
377 "map" => return Ok(HeaderLine::MapFollows),
378 other => return Err(format!("unknown key {other:?}")),
379 }
380 Ok(HeaderLine::Read)
381 }
382}
383
384fn parse_header(lines: &mut std::str::Lines) -> Result<Header, String> {
386 let mut header = Header::default();
387 for line in lines.by_ref() {
388 let line = line.trim();
389 if line.is_empty() || line.starts_with('#') {
391 continue;
392 }
393 let Some((key, value)) = line.split_once(':') else {
394 return Err(format!("expected `key: value` before map, got {line:?}"));
395 };
396 if let HeaderLine::MapFollows = header.read_line(key.trim(), value.trim())? {
397 break;
398 }
399 }
400 Ok(header)
401}
402
403fn parse_lattice<'a>(lines: impl Iterator<Item = &'a str>, seed: u64) -> Result<Board, String> {
405 let lattice: Vec<&str> = lines.filter(|l: &&str| !l.trim().is_empty()).collect();
406 if lattice.is_empty() {
407 return Err("missing map section".into());
408 }
409 let lat_h = lattice.len();
410 let lat_w = lattice[0].chars().count();
411 if lat_h.is_multiple_of(2) || lat_w.is_multiple_of(2) {
412 return Err(format!(
413 "map lattice must be odd-sized, got {lat_w}×{lat_h}"
414 ));
415 }
416 if lat_w < 3 || lat_h < 3 {
423 return Err(format!(
424 "map lattice must hold at least one tile, got {lat_w}×{lat_h}"
425 ));
426 }
427 let (tiles_w, tiles_h) = (lat_w / 2, lat_h / 2);
428 let (Ok(width), Ok(height)) = (u8::try_from(tiles_w), u8::try_from(tiles_h)) else {
429 return Err(format!(
430 "map is {tiles_w}×{tiles_h} tiles, past the {} a side the format can name",
431 u8::MAX
432 ));
433 };
434 if let Some((row, width)) = lattice
438 .iter()
439 .map(|row| row.chars().count())
440 .enumerate()
441 .find(|(_, width)| *width > lat_w)
442 {
443 return Err(format!(
444 "map row {row} is {width} wide, past the top border's {lat_w}"
445 ));
446 }
447 let grid: Vec<Vec<char>> = lattice
451 .iter()
452 .map(|row| {
453 let mut chars: Vec<char> = row.chars().collect();
454 chars.resize(lat_w, ' ');
455 chars
456 })
457 .collect();
458
459 debug_assert!(width > 0 && height > 0, "a {width}x{height} board");
465 let mut board = Board::new(width, height, seed);
466 for y in 0..height {
467 for x in 0..width {
468 let c = grid[y as usize * 2 + 1][x as usize * 2 + 1];
469 match tile_from_glyph(c) {
470 Some(TileKind::Empty) => {}
471 Some(tile) => board.set_tile(x, y, tile),
472 None => return Err(format!("bad tile char {c:?} at ({x},{y})")),
473 }
474 if grid[y as usize * 2][x as usize * 2 + 1] == '-' {
476 board.set_wall(x, y, Direction::Up, true);
477 }
478 if grid[y as usize * 2 + 1][x as usize * 2] == '|' {
480 board.set_wall(x, y, Direction::Left, true);
481 }
482 }
483 }
484 for x in 0..width {
486 if grid[height as usize * 2][x as usize * 2 + 1] == '-' {
487 board.set_wall(x, height - 1, Direction::Down, true);
488 }
489 }
490 for y in 0..height {
491 if grid[y as usize * 2 + 1][width as usize * 2] == '|' {
492 board.set_wall(width - 1, y, Direction::Right, true);
493 }
494 }
495 Ok(board)
496}
497
498fn place_entities(board: &mut Board, header: &Header) -> Result<(), String> {
501 let (width, height) = (board.width(), board.height());
502 let check = |what: &str, x: u8, y: u8| -> Result<(), String> {
503 if x >= width || y >= height {
504 return Err(format!(
505 "{what} at ({x},{y}) is off the {width}x{height} board"
506 ));
507 }
508 Ok(())
509 };
510 for &(x, y, dir, period) in &header.spawners {
511 check("spawner", x, y)?;
512 if period == 0 {
513 return Err(format!("spawner at ({x},{y}): period must be at least 1"));
514 }
515 board.set_tile(x, y, TileKind::Spawner(Spawner { dir, period }));
516 }
517 for &(x, y, dir, handed, kind) in &header.crabs {
518 check("crab", x, y)?;
519 if board.tile_at(x, y) == TileKind::Rock {
520 return Err(format!("crab at ({x},{y}) is standing on a rock"));
521 }
522 board.spawn_crab(x, y, dir, handed, kind);
523 }
524 for &(x, y, dir) in &header.gulls {
525 check("gull", x, y)?;
526 if !board.gull_may_stand(x, y) {
527 return Err(format!(
528 "gull at ({x},{y}) is standing on a rock or in kelp"
529 ));
530 }
531 board.spawn_gull(x, y, dir);
532 }
533 board.set_gull_period(header.gull_period);
534 board.set_round_length(header.round);
535 if header.no_castle_raids {
536 board.set_castle_raids(false);
537 }
538 if header.wrap {
539 board.set_wrap(true);
540 }
541 if header.events {
542 board.set_events_enabled(true);
543 }
544 if let Some((cap, policy)) = header.rule {
545 board.set_signpost_rule(cap, policy);
546 }
547 for &(player, score) in &header.scores {
548 board.set_score(player, score);
549 }
550 Ok(())
551}
552
553fn tile_glyph(tile: TileKind) -> char {
556 match tile {
557 TileKind::Empty | TileKind::Spawner(_) => '.',
558 TileKind::Rock => '#',
559 TileKind::Castle(owner) => (b'0' + owner) as char,
560 TileKind::Turnstile { next_right: true } => 'T',
565 TileKind::Turnstile { next_right: false } => 't',
566 TileKind::Kelp => 'K',
567 TileKind::Pool => '~',
568 }
569}
570
571fn tile_from_glyph(c: char) -> Option<TileKind> {
572 match c {
573 '.' | ' ' => Some(TileKind::Empty),
574 '#' => Some(TileKind::Rock),
575 'T' => Some(TileKind::Turnstile { next_right: true }),
576 't' => Some(TileKind::Turnstile { next_right: false }),
577 'K' => Some(TileKind::Kelp),
578 '~' => Some(TileKind::Pool),
579 '0'..='9' if crate::sim::board::seat(c as u8 - b'0').is_some() => {
583 Some(TileKind::Castle((c as u8 - b'0') as PlayerId))
584 }
585 _ => None,
586 }
587}
588
589fn parse_dir(s: &str) -> Result<Direction, String> {
590 Direction::from_letter(s).ok_or_else(|| format!("bad direction {s:?}"))
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596
597 fn level_with(header: &str) -> Result<Level, String> {
600 let text =
601 format!("name: T\nposts: 1\n{header}\nmap:\n+-+-+\n|.|.|\n+-+-+\n|.|#|\n+-+-+\n");
602 Level::parse(&text)
603 }
604
605 #[test]
612 fn placing_an_entity_where_it_cannot_go_is_refused_with_the_reason() {
613 let err = level_with("spawner: 0,0 R 0").unwrap_err();
614 assert!(err.contains("period must be at least 1"), "{err}");
615 let err = level_with("rule: evict 0").unwrap_err();
616 assert!(err.contains("at least 1"), "{err}");
617 assert!(
618 level_with("rule: reject 0").is_ok(),
619 "no posts to give is a legal puzzle"
620 );
621 let err = level_with("crab: 1,1 R L common").unwrap_err();
622 assert!(err.contains("standing on a rock"), "{err}");
623 let err = level_with("gull: 1,1 R").unwrap_err();
624 assert!(err.contains("standing on a rock"), "{err}");
625 let kelp = "name: T\nposts: 1\ngull: 1,1 R\nmap:\n+-+-+\n|.|.|\n+-+-+\n|.|K|\n+-+-+\n";
628 let err = Level::parse(kelp).unwrap_err();
629 assert!(err.contains("in kelp"), "{err}");
630 for line in ["spawner: 2,0 R 4", "crab: 0,2 R L common", "gull: 5,5 D"] {
631 let err = level_with(line).unwrap_err();
632 assert!(err.contains("off the 2x2 board"), "{line}: {err}");
633 }
634 let level = level_with("spawner: 0,0 R 4\ncrab: 1,0 R L common\ngull: 0,1 R")
636 .expect("a valid placement");
637 assert_eq!(level.board.crabs().len(), 1);
638 assert_eq!(level.board.gulls().len(), 1);
639 assert!(matches!(level.board.tile_at(0, 0), TileKind::Spawner(_)));
640 }
641}