Initial parser for parsing text into ZZT/BRD files
I ran some limited tests, and the important data seems to survive roundtrip conversions. It's a bit tricky to measure because conversion to text strips out/normalizes some data, such as garbage bytes in the unused portions of Pascal strings.
This commit is contained in:
+684
-1
@@ -1,6 +1,17 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use super::elements::{Element, element_name};
|
||||
use nom::{
|
||||
IResult, Parser,
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_until, take_while, take_while1},
|
||||
character::complete::{char, digit1, line_ending, multispace0, multispace1, not_line_ending},
|
||||
combinator::{map, map_res, opt, recognize, value},
|
||||
multi::many0,
|
||||
sequence::pair,
|
||||
};
|
||||
|
||||
use super::elements::{Element, element_name, resolve_alias};
|
||||
use super::error::ParseError;
|
||||
use super::parse::{Board, Program, Stat, Tile, World};
|
||||
|
||||
/// Write key = value if value != default.
|
||||
@@ -214,3 +225,675 @@ fn write_param(output: &mut String, value: u8, generic_name: &str, alias: Option
|
||||
let name = alias.unwrap_or(generic_name);
|
||||
writeln!(output, "{} = {}", name, value).unwrap();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Text Parsing
|
||||
// ============================================================================
|
||||
|
||||
/// Parsed value from a key-value pair.
|
||||
#[derive(Debug, Clone)]
|
||||
enum Value {
|
||||
Int(i64),
|
||||
Bool(bool),
|
||||
String(String),
|
||||
StringArray(Vec<String>),
|
||||
Tuple2(u8, u8),
|
||||
TripleQuotedString(String),
|
||||
}
|
||||
|
||||
/// Skip a comment (# to end of line).
|
||||
fn comment(input: &str) -> IResult<&str, ()> {
|
||||
value((), pair(char('#'), not_line_ending)).parse(input)
|
||||
}
|
||||
|
||||
/// Skip whitespace and comments.
|
||||
fn ws(input: &str) -> IResult<&str, ()> {
|
||||
value(
|
||||
(),
|
||||
many0(alt((value((), multispace1), comment))),
|
||||
).parse(input)
|
||||
}
|
||||
|
||||
/// Parse a section header like [world], [board 0], [stat 1].
|
||||
/// Returns the optional index.
|
||||
fn section_header<'a>(name: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, Option<usize>> {
|
||||
move |input: &'a str| {
|
||||
let (input, _) = ws(input)?;
|
||||
let (input, _) = char('[').parse(input)?;
|
||||
let (input, _) = tag(name).parse(input)?;
|
||||
let (input, idx) = opt((multispace1, map_res(digit1, |s: &str| s.parse())))
|
||||
.map(|opt| opt.map(|(_, n)| n))
|
||||
.parse(input)?;
|
||||
let (input, _) = char(']').parse(input)?;
|
||||
// Consume rest of line (including any comment)
|
||||
let (input, _) = opt(pair(take_while(|c| c == ' ' || c == '\t'), comment)).parse(input)?;
|
||||
let (input, _) = opt(line_ending).parse(input)?;
|
||||
Ok((input, idx))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an identifier (key name).
|
||||
fn identifier(input: &str) -> IResult<&str, &str> {
|
||||
take_while1(|c: char| c.is_alphanumeric() || c == '_').parse(input)
|
||||
}
|
||||
|
||||
/// Parse a quoted string value (handles escape sequences).
|
||||
fn quoted_string(input: &str) -> IResult<&str, String> {
|
||||
let (input, _) = char('"').parse(input)?;
|
||||
let mut result = String::new();
|
||||
let mut chars = input.chars().peekable();
|
||||
let mut consumed = 0;
|
||||
|
||||
loop {
|
||||
match chars.next() {
|
||||
Some('"') => {
|
||||
consumed += 1;
|
||||
break;
|
||||
}
|
||||
Some('\\') => {
|
||||
consumed += 1;
|
||||
match chars.next() {
|
||||
Some('n') => {
|
||||
result.push('\n');
|
||||
consumed += 1;
|
||||
}
|
||||
Some('r') => {
|
||||
result.push('\r');
|
||||
consumed += 1;
|
||||
}
|
||||
Some('t') => {
|
||||
result.push('\t');
|
||||
consumed += 1;
|
||||
}
|
||||
Some('\\') => {
|
||||
result.push('\\');
|
||||
consumed += 1;
|
||||
}
|
||||
Some('"') => {
|
||||
result.push('"');
|
||||
consumed += 1;
|
||||
}
|
||||
Some(c) => {
|
||||
// Unknown escape, keep as-is
|
||||
result.push('\\');
|
||||
result.push(c);
|
||||
consumed += 1;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Some(c) => {
|
||||
result.push(c);
|
||||
consumed += c.len_utf8();
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok((&input[consumed..], result))
|
||||
}
|
||||
|
||||
/// Parse a triple-quoted string ("""...""").
|
||||
fn triple_quoted_string(input: &str) -> IResult<&str, String> {
|
||||
let (input, _) = tag("\"\"\"").parse(input)?;
|
||||
let (input, _) = opt(line_ending).parse(input)?;
|
||||
let (input, content) = take_until("\"\"\"").parse(input)?;
|
||||
let (input, _) = tag("\"\"\"").parse(input)?;
|
||||
// Trim trailing newline from content if present
|
||||
let content = content.strip_suffix('\n').unwrap_or(content);
|
||||
Ok((input, content.to_string()))
|
||||
}
|
||||
|
||||
/// Parse an integer (possibly negative).
|
||||
fn integer(input: &str) -> IResult<&str, i64> {
|
||||
map_res(
|
||||
recognize(pair(opt(char('-')), digit1)),
|
||||
|s: &str| s.parse(),
|
||||
).parse(input)
|
||||
}
|
||||
|
||||
/// Parse a boolean value.
|
||||
fn boolean(input: &str) -> IResult<&str, bool> {
|
||||
alt((value(true, tag("true")), value(false, tag("false")))).parse(input)
|
||||
}
|
||||
|
||||
/// Parse a tuple like (element, color).
|
||||
fn tuple2(input: &str) -> IResult<&str, (u8, u8)> {
|
||||
let (input, _) = char('(').parse(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
let (input, a) = map_res(digit1, |s: &str| s.parse::<u8>()).parse(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
let (input, _) = char(',').parse(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
let (input, b) = map_res(digit1, |s: &str| s.parse::<u8>()).parse(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
let (input, _) = char(')').parse(input)?;
|
||||
Ok((input, (a, b)))
|
||||
}
|
||||
|
||||
/// Parse a string array like ["foo", "bar", ""].
|
||||
fn string_array(input: &str) -> IResult<&str, Vec<String>> {
|
||||
let (input, _) = char('[').parse(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
|
||||
let mut items = Vec::new();
|
||||
let mut input = input;
|
||||
|
||||
// Check for empty array
|
||||
if let Ok((next, _)) = char::<_, nom::error::Error<&str>>(']').parse(input) {
|
||||
return Ok((next, items));
|
||||
}
|
||||
|
||||
// Parse first item
|
||||
let (next, first) = quoted_string(input)?;
|
||||
items.push(first);
|
||||
input = next;
|
||||
|
||||
// Parse remaining items
|
||||
loop {
|
||||
let (next, _) = multispace0.parse(input)?;
|
||||
if let Ok((next, _)) = char::<_, nom::error::Error<&str>>(']').parse(next) {
|
||||
return Ok((next, items));
|
||||
}
|
||||
let (next, _) = char(',').parse(next)?;
|
||||
let (next, _) = multispace0.parse(next)?;
|
||||
let (next, item) = quoted_string(next)?;
|
||||
items.push(item);
|
||||
input = next;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a value (any type).
|
||||
fn parse_value(input: &str) -> IResult<&str, Value> {
|
||||
alt((
|
||||
map(triple_quoted_string, Value::TripleQuotedString),
|
||||
map(string_array, Value::StringArray),
|
||||
map(quoted_string, Value::String),
|
||||
map(tuple2, |(a, b)| Value::Tuple2(a, b)),
|
||||
map(boolean, Value::Bool),
|
||||
map(integer, Value::Int),
|
||||
)).parse(input)
|
||||
}
|
||||
|
||||
/// Parse a key = value pair.
|
||||
fn parse_key_value(input: &str) -> IResult<&str, (&str, Value)> {
|
||||
let (input, _) = ws(input)?;
|
||||
let (input, key) = identifier(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
let (input, _) = char('=').parse(input)?;
|
||||
let (input, _) = multispace0.parse(input)?;
|
||||
let (input, value) = parse_value(input)?;
|
||||
// Consume rest of line (including any comment)
|
||||
let (input, _) = opt(pair(take_while(|c| c == ' ' || c == '\t'), comment)).parse(input)?;
|
||||
let (input, _) = opt(line_ending).parse(input)?;
|
||||
Ok((input, (key, value)))
|
||||
}
|
||||
|
||||
/// Parse 25 rows of hex data (60 columns each, 2 hex digits per value).
|
||||
fn parse_hex_grid(input: &str) -> Result<(&str, Vec<u8>), ParseError> {
|
||||
let mut result = Vec::with_capacity(1500);
|
||||
let mut input = input;
|
||||
|
||||
for _ in 0..25 {
|
||||
// Skip whitespace/comments before the row
|
||||
let (next, _) = ws(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("hex grid whitespace: {:?}", e),
|
||||
})?;
|
||||
input = next;
|
||||
|
||||
// Parse 60 hex pairs
|
||||
for _ in 0..60 {
|
||||
if input.len() < 2 {
|
||||
return Err(ParseError::InvalidHex("unexpected end of hex data".to_string()));
|
||||
}
|
||||
let hex_str = &input[..2];
|
||||
let byte = u8::from_str_radix(hex_str, 16)
|
||||
.map_err(|_| ParseError::InvalidHex(hex_str.to_string()))?;
|
||||
result.push(byte);
|
||||
input = &input[2..];
|
||||
}
|
||||
|
||||
// Consume newline if present
|
||||
if input.starts_with('\n') {
|
||||
input = &input[1..];
|
||||
} else if input.starts_with("\r\n") {
|
||||
input = &input[2..];
|
||||
}
|
||||
}
|
||||
|
||||
Ok((input, result))
|
||||
}
|
||||
|
||||
/// Parse terrain: element grid followed by color grid.
|
||||
fn parse_terrain(input: &str) -> Result<(&str, Vec<Tile>), ParseError> {
|
||||
let (input, elements) = parse_hex_grid(input)?;
|
||||
let (input, colors) = parse_hex_grid(input)?;
|
||||
|
||||
let tiles: Vec<Tile> = elements
|
||||
.into_iter()
|
||||
.zip(colors)
|
||||
.map(|(element, color)| Tile { element, color })
|
||||
.collect();
|
||||
|
||||
Ok((input, tiles))
|
||||
}
|
||||
|
||||
/// Parse keys string like "bgcr" into bool array.
|
||||
fn parse_keys(s: &str) -> [bool; 7] {
|
||||
let key_chars = ['b', 'g', 'c', 'r', 'p', 'y', 'w'];
|
||||
let mut keys = [false; 7];
|
||||
for (i, c) in key_chars.iter().enumerate() {
|
||||
keys[i] = s.contains(*c);
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
/// Check if the next non-whitespace is a section header.
|
||||
fn peek_section(input: &str) -> bool {
|
||||
let trimmed = input.trim_start();
|
||||
trimmed.starts_with('[')
|
||||
}
|
||||
|
||||
/// Check if input is at end or only has whitespace/comments.
|
||||
fn at_end(input: &str) -> bool {
|
||||
match ws(input) {
|
||||
Ok((rest, _)) => rest.is_empty(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the [world] section properties.
|
||||
fn parse_world_section(input: &str) -> Result<(&str, World), ParseError> {
|
||||
let (input, _) = section_header("world")(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("expected [world] section: {:?}", e),
|
||||
})?;
|
||||
|
||||
let mut world = World {
|
||||
health: 100,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut input = input;
|
||||
while !at_end(input) && !peek_section(input) {
|
||||
let (next, (key, value)) =
|
||||
parse_key_value(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("world property: {:?}", e),
|
||||
})?;
|
||||
input = next;
|
||||
|
||||
match key {
|
||||
"name" => {
|
||||
if let Value::String(s) = value {
|
||||
world.name = s;
|
||||
}
|
||||
}
|
||||
"health" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.health = n as i16;
|
||||
}
|
||||
}
|
||||
"ammo" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.ammo = n as i16;
|
||||
}
|
||||
}
|
||||
"gems" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.gems = n as i16;
|
||||
}
|
||||
}
|
||||
"torches" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.torches = n as i16;
|
||||
}
|
||||
}
|
||||
"score" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.score = n as i16;
|
||||
}
|
||||
}
|
||||
"keys" => {
|
||||
if let Value::String(s) = value {
|
||||
world.keys = parse_keys(&s);
|
||||
}
|
||||
}
|
||||
"starting_board" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.starting_board = n as i16;
|
||||
}
|
||||
}
|
||||
"saved_game" => {
|
||||
if let Value::Bool(b) = value {
|
||||
world.locked = b;
|
||||
}
|
||||
}
|
||||
"flags" => {
|
||||
if let Value::StringArray(arr) = value {
|
||||
for (i, flag) in arr.into_iter().take(10).enumerate() {
|
||||
world.flags[i] = flag;
|
||||
}
|
||||
}
|
||||
}
|
||||
"torch_cycles" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.torch_cycles = n as i16;
|
||||
}
|
||||
}
|
||||
"energizer_cycles" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.energizer_cycles = n as i16;
|
||||
}
|
||||
}
|
||||
"time" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.time = n as i16;
|
||||
}
|
||||
}
|
||||
"time_ticks" => {
|
||||
if let Value::Int(n) = value {
|
||||
world.time_ticks = n as i16;
|
||||
}
|
||||
}
|
||||
_ => {} // Ignore unknown keys
|
||||
}
|
||||
}
|
||||
|
||||
Ok((input, world))
|
||||
}
|
||||
|
||||
/// Get element at (x, y) from tiles array.
|
||||
fn element_at(tiles: &[Tile], x: u8, y: u8) -> Option<u8> {
|
||||
if x == 0 || y == 0 || x > 60 || y > 25 {
|
||||
return None;
|
||||
}
|
||||
let index = ((y as usize - 1) * 60) + (x as usize - 1);
|
||||
tiles.get(index).map(|t| t.element)
|
||||
}
|
||||
|
||||
/// Parse a [stat N] section.
|
||||
fn parse_stat_section<'a>(input: &'a str, tiles: &[Tile]) -> Result<(&'a str, Stat), ParseError> {
|
||||
let (input, _) = section_header("stat")(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("expected [stat] section: {:?}", e),
|
||||
})?;
|
||||
|
||||
// First pass: collect all key-value pairs
|
||||
let mut pairs: Vec<(&str, Value)> = Vec::new();
|
||||
let mut input = input;
|
||||
while !at_end(input) && !peek_section(input) {
|
||||
let (next, pair) = parse_key_value(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("stat property: {:?}", e),
|
||||
})?;
|
||||
pairs.push(pair);
|
||||
input = next;
|
||||
}
|
||||
|
||||
// Extract x, y first to determine element type
|
||||
let mut x: u8 = 0;
|
||||
let mut y: u8 = 0;
|
||||
for (key, value) in &pairs {
|
||||
match *key {
|
||||
"x" => {
|
||||
if let Value::Int(n) = value {
|
||||
x = *n as u8;
|
||||
}
|
||||
}
|
||||
"y" => {
|
||||
if let Value::Int(n) = value {
|
||||
y = *n as u8;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let element = element_at(tiles, x, y).and_then(Element::from_u8);
|
||||
|
||||
// Build stat with defaults
|
||||
let mut stat = Stat {
|
||||
x,
|
||||
y,
|
||||
x_step: 0,
|
||||
y_step: 0,
|
||||
cycle: 0,
|
||||
p1: 0,
|
||||
p2: 0,
|
||||
p3: 0,
|
||||
follower: -1,
|
||||
leader: -1,
|
||||
under: Tile {
|
||||
element: 0,
|
||||
color: 0,
|
||||
},
|
||||
instruction_pointer: 0,
|
||||
program: Program::Own(String::new()),
|
||||
};
|
||||
|
||||
// Second pass: apply all properties
|
||||
for (key, value) in pairs {
|
||||
match key {
|
||||
"x" | "y" => {} // Already handled
|
||||
"cycle" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.cycle = n as i16;
|
||||
}
|
||||
}
|
||||
"x_step" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.x_step = n as i16;
|
||||
}
|
||||
}
|
||||
"y_step" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.y_step = n as i16;
|
||||
}
|
||||
}
|
||||
"under" => {
|
||||
if let Value::Tuple2(e, c) = value {
|
||||
stat.under = Tile {
|
||||
element: e,
|
||||
color: c,
|
||||
};
|
||||
}
|
||||
}
|
||||
"follower" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.follower = n as i16;
|
||||
}
|
||||
}
|
||||
"leader" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.leader = n as i16;
|
||||
}
|
||||
}
|
||||
"instruction_pointer" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.instruction_pointer = n as i16;
|
||||
}
|
||||
}
|
||||
"p1" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.p1 = n as u8;
|
||||
}
|
||||
}
|
||||
"p2" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.p2 = n as u8;
|
||||
}
|
||||
}
|
||||
"p3" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.p3 = n as u8;
|
||||
}
|
||||
}
|
||||
"code" => {
|
||||
if let Value::TripleQuotedString(s) = value {
|
||||
stat.program = Program::Own(s);
|
||||
}
|
||||
}
|
||||
"bind" => {
|
||||
if let Value::Int(n) = value {
|
||||
stat.program = Program::Bound(n as u16);
|
||||
}
|
||||
}
|
||||
other => {
|
||||
// Check if it's a parameter alias
|
||||
if let Some(param_num) = resolve_alias(other, element) {
|
||||
if let Value::Int(n) = value {
|
||||
match param_num {
|
||||
1 => stat.p1 = n as u8,
|
||||
2 => stat.p2 = n as u8,
|
||||
3 => stat.p3 = n as u8,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore unknown keys
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((input, stat))
|
||||
}
|
||||
|
||||
/// Parse a [board N] section.
|
||||
fn parse_board_section(input: &str) -> Result<(&str, Board), ParseError> {
|
||||
let (input, _) = section_header("board")(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("expected [board] section: {:?}", e),
|
||||
})?;
|
||||
|
||||
// Parse title first (required before terrain)
|
||||
let (input, (_, title_value)) =
|
||||
parse_key_value(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("expected board title: {:?}", e),
|
||||
})?;
|
||||
|
||||
let title = match title_value {
|
||||
Value::String(s) => s,
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
// Parse terrain
|
||||
let (input, tiles) = parse_terrain(input)?;
|
||||
|
||||
// Parse board properties
|
||||
let mut board = Board {
|
||||
name: title,
|
||||
tiles,
|
||||
max_shots: 0,
|
||||
is_dark: false,
|
||||
exit_north: 0,
|
||||
exit_south: 0,
|
||||
exit_west: 0,
|
||||
exit_east: 0,
|
||||
restart_on_zap: false,
|
||||
message: String::new(),
|
||||
enter_x: 0,
|
||||
enter_y: 0,
|
||||
time_limit: 0,
|
||||
stats: Vec::new(),
|
||||
};
|
||||
|
||||
let mut input = input;
|
||||
|
||||
// Parse remaining properties until we hit a stat section or end
|
||||
while !at_end(input) && !peek_section(input) {
|
||||
let (next, (key, value)) =
|
||||
parse_key_value(input).map_err(|e| ParseError::TextParseError {
|
||||
message: format!("board property: {:?}", e),
|
||||
})?;
|
||||
input = next;
|
||||
|
||||
match key {
|
||||
"shots" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.max_shots = n as u8;
|
||||
}
|
||||
}
|
||||
"dark" => {
|
||||
if let Value::Bool(b) = value {
|
||||
board.is_dark = b;
|
||||
}
|
||||
}
|
||||
"exit_n" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.exit_north = n as u8;
|
||||
}
|
||||
}
|
||||
"exit_s" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.exit_south = n as u8;
|
||||
}
|
||||
}
|
||||
"exit_e" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.exit_east = n as u8;
|
||||
}
|
||||
}
|
||||
"exit_w" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.exit_west = n as u8;
|
||||
}
|
||||
}
|
||||
"reenter" => {
|
||||
if let Value::Bool(b) = value {
|
||||
board.restart_on_zap = b;
|
||||
}
|
||||
}
|
||||
"time_limit" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.time_limit = n as i16;
|
||||
}
|
||||
}
|
||||
"enter_x" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.enter_x = n as u8;
|
||||
}
|
||||
}
|
||||
"enter_y" => {
|
||||
if let Value::Int(n) = value {
|
||||
board.enter_y = n as u8;
|
||||
}
|
||||
}
|
||||
"message" => {
|
||||
if let Value::String(s) = value {
|
||||
board.message = s;
|
||||
}
|
||||
}
|
||||
_ => {} // Ignore unknown keys
|
||||
}
|
||||
}
|
||||
|
||||
// Parse stats
|
||||
while !at_end(input) {
|
||||
// Check if next section is a stat
|
||||
let trimmed = input.trim_start();
|
||||
if !trimmed.starts_with("[stat") {
|
||||
break;
|
||||
}
|
||||
let (next, stat) = parse_stat_section(input, &board.tiles)?;
|
||||
board.stats.push(stat);
|
||||
input = next;
|
||||
}
|
||||
|
||||
Ok((input, board))
|
||||
}
|
||||
|
||||
/// Convert text to a World.
|
||||
pub fn text_to_world(text: &str) -> Result<World, ParseError> {
|
||||
let (input, mut world) = parse_world_section(text)?;
|
||||
|
||||
let mut input = input;
|
||||
while !at_end(input) {
|
||||
let (next, board) = parse_board_section(input)?;
|
||||
world.boards.push(board);
|
||||
input = next;
|
||||
}
|
||||
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
/// Convert text to a Board.
|
||||
pub fn text_to_board(text: &str) -> Result<Board, ParseError> {
|
||||
let (_, board) = parse_board_section(text)?;
|
||||
Ok(board)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user