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:
@@ -217,6 +217,54 @@ impl Element {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map alias name to parameter number (1, 2, or 3) based on element type.
|
||||||
|
pub fn alias_to_param(self, alias: &str) -> Option<u8> {
|
||||||
|
if self.p1_alias() == Some(alias) {
|
||||||
|
Some(1)
|
||||||
|
} else if self.p2_alias() == Some(alias) {
|
||||||
|
Some(2)
|
||||||
|
} else if self.p3_alias() == Some(alias) {
|
||||||
|
Some(3)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve alias name to parameter number, with optional element context.
|
||||||
|
/// Falls back to checking all known aliases if element is None.
|
||||||
|
pub fn resolve_alias(alias: &str, element: Option<Element>) -> Option<u8> {
|
||||||
|
// If we have an element, use its specific alias mapping
|
||||||
|
if let Some(elem) = element {
|
||||||
|
if let Some(param) = elem.alias_to_param(alias) {
|
||||||
|
return Some(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check all known aliases
|
||||||
|
// p1 aliases
|
||||||
|
if matches!(
|
||||||
|
alias,
|
||||||
|
"char" | "sensitivity" | "intelligence" | "start_time"
|
||||||
|
) {
|
||||||
|
return Some(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// p2 aliases
|
||||||
|
if matches!(
|
||||||
|
alias,
|
||||||
|
"rate" | "period" | "resting_time" | "speed" | "firing_rate" | "deviance"
|
||||||
|
) {
|
||||||
|
return Some(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// p3 aliases
|
||||||
|
if matches!(alias, "destination" | "firing_type") {
|
||||||
|
return Some(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the human-readable name for an element ID, or describe unknown elements.
|
/// Get the human-readable name for an element ID, or describe unknown elements.
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ pub enum ParseError {
|
|||||||
|
|
||||||
#[error("parse error: {0}")]
|
#[error("parse error: {0}")]
|
||||||
ParseError(String),
|
ParseError(String),
|
||||||
|
|
||||||
|
#[error("text parse error: {message}")]
|
||||||
|
TextParseError { message: String },
|
||||||
|
|
||||||
|
#[error("invalid hex in terrain: {0}")]
|
||||||
|
InvalidHex(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I> NomParseError<I> for ParseError {
|
impl<I> NomParseError<I> for ParseError {
|
||||||
|
|||||||
+1
-1
@@ -7,4 +7,4 @@ mod text;
|
|||||||
pub use encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
pub use encoding::{decode_multiline, decode_oneline, encode_multiline, encode_oneline};
|
||||||
pub use error::ParseError;
|
pub use error::ParseError;
|
||||||
pub use parse::{Board, Program, Stat, Tile, World};
|
pub use parse::{Board, Program, Stat, Tile, World};
|
||||||
pub use text::{board_to_text, world_to_text};
|
pub use text::{board_to_text, text_to_board, text_to_world, world_to_text};
|
||||||
|
|||||||
+684
-1
@@ -1,6 +1,17 @@
|
|||||||
use std::fmt::Write;
|
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};
|
use super::parse::{Board, Program, Stat, Tile, World};
|
||||||
|
|
||||||
/// Write key = value if value != default.
|
/// 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);
|
let name = alias.unwrap_or(generic_name);
|
||||||
writeln!(output, "{} = {}", name, value).unwrap();
|
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