Add initial text format for .ZZT/.BRD
This commit is contained in:
@@ -0,0 +1,228 @@
|
|||||||
|
/// ZZT element types.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Element {
|
||||||
|
Empty = 0,
|
||||||
|
BoardEdge = 1,
|
||||||
|
Messenger = 2,
|
||||||
|
Monitor = 3,
|
||||||
|
Player = 4,
|
||||||
|
Ammo = 5,
|
||||||
|
Torch = 6,
|
||||||
|
Gem = 7,
|
||||||
|
Key = 8,
|
||||||
|
Door = 9,
|
||||||
|
Scroll = 10,
|
||||||
|
Passage = 11,
|
||||||
|
Duplicator = 12,
|
||||||
|
Bomb = 13,
|
||||||
|
Energizer = 14,
|
||||||
|
Star = 15,
|
||||||
|
Clockwise = 16,
|
||||||
|
Counter = 17,
|
||||||
|
Bullet = 18,
|
||||||
|
Water = 19,
|
||||||
|
Forest = 20,
|
||||||
|
Solid = 21,
|
||||||
|
Normal = 22,
|
||||||
|
Breakable = 23,
|
||||||
|
Boulder = 24,
|
||||||
|
SliderNs = 25,
|
||||||
|
SliderEw = 26,
|
||||||
|
Fake = 27,
|
||||||
|
Invisible = 28,
|
||||||
|
BlinkWall = 29,
|
||||||
|
Transporter = 30,
|
||||||
|
Line = 31,
|
||||||
|
Ricochet = 32,
|
||||||
|
BlinkRayH = 33,
|
||||||
|
Bear = 34,
|
||||||
|
Ruffian = 35,
|
||||||
|
Object = 36,
|
||||||
|
Slime = 37,
|
||||||
|
Shark = 38,
|
||||||
|
SpinningGun = 39,
|
||||||
|
Pusher = 40,
|
||||||
|
Lion = 41,
|
||||||
|
Tiger = 42,
|
||||||
|
BlinkRayV = 43,
|
||||||
|
Head = 44,
|
||||||
|
Segment = 45,
|
||||||
|
// 46 is unused
|
||||||
|
TextBlue = 47,
|
||||||
|
TextGreen = 48,
|
||||||
|
TextCyan = 49,
|
||||||
|
TextRed = 50,
|
||||||
|
TextPurple = 51,
|
||||||
|
TextBrown = 52,
|
||||||
|
TextBlack = 53,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element {
|
||||||
|
/// Try to convert a u8 to an Element.
|
||||||
|
pub fn from_u8(id: u8) -> Option<Element> {
|
||||||
|
match id {
|
||||||
|
0 => Some(Element::Empty),
|
||||||
|
1 => Some(Element::BoardEdge),
|
||||||
|
2 => Some(Element::Messenger),
|
||||||
|
3 => Some(Element::Monitor),
|
||||||
|
4 => Some(Element::Player),
|
||||||
|
5 => Some(Element::Ammo),
|
||||||
|
6 => Some(Element::Torch),
|
||||||
|
7 => Some(Element::Gem),
|
||||||
|
8 => Some(Element::Key),
|
||||||
|
9 => Some(Element::Door),
|
||||||
|
10 => Some(Element::Scroll),
|
||||||
|
11 => Some(Element::Passage),
|
||||||
|
12 => Some(Element::Duplicator),
|
||||||
|
13 => Some(Element::Bomb),
|
||||||
|
14 => Some(Element::Energizer),
|
||||||
|
15 => Some(Element::Star),
|
||||||
|
16 => Some(Element::Clockwise),
|
||||||
|
17 => Some(Element::Counter),
|
||||||
|
18 => Some(Element::Bullet),
|
||||||
|
19 => Some(Element::Water),
|
||||||
|
20 => Some(Element::Forest),
|
||||||
|
21 => Some(Element::Solid),
|
||||||
|
22 => Some(Element::Normal),
|
||||||
|
23 => Some(Element::Breakable),
|
||||||
|
24 => Some(Element::Boulder),
|
||||||
|
25 => Some(Element::SliderNs),
|
||||||
|
26 => Some(Element::SliderEw),
|
||||||
|
27 => Some(Element::Fake),
|
||||||
|
28 => Some(Element::Invisible),
|
||||||
|
29 => Some(Element::BlinkWall),
|
||||||
|
30 => Some(Element::Transporter),
|
||||||
|
31 => Some(Element::Line),
|
||||||
|
32 => Some(Element::Ricochet),
|
||||||
|
33 => Some(Element::BlinkRayH),
|
||||||
|
34 => Some(Element::Bear),
|
||||||
|
35 => Some(Element::Ruffian),
|
||||||
|
36 => Some(Element::Object),
|
||||||
|
37 => Some(Element::Slime),
|
||||||
|
38 => Some(Element::Shark),
|
||||||
|
39 => Some(Element::SpinningGun),
|
||||||
|
40 => Some(Element::Pusher),
|
||||||
|
41 => Some(Element::Lion),
|
||||||
|
42 => Some(Element::Tiger),
|
||||||
|
43 => Some(Element::BlinkRayV),
|
||||||
|
44 => Some(Element::Head),
|
||||||
|
45 => Some(Element::Segment),
|
||||||
|
47 => Some(Element::TextBlue),
|
||||||
|
48 => Some(Element::TextGreen),
|
||||||
|
49 => Some(Element::TextCyan),
|
||||||
|
50 => Some(Element::TextRed),
|
||||||
|
51 => Some(Element::TextPurple),
|
||||||
|
52 => Some(Element::TextBrown),
|
||||||
|
53 => Some(Element::TextBlack),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the human-readable name for this element.
|
||||||
|
pub fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Element::Empty => "empty",
|
||||||
|
Element::BoardEdge => "board_edge",
|
||||||
|
Element::Messenger => "messenger",
|
||||||
|
Element::Monitor => "monitor",
|
||||||
|
Element::Player => "player",
|
||||||
|
Element::Ammo => "ammo",
|
||||||
|
Element::Torch => "torch",
|
||||||
|
Element::Gem => "gem",
|
||||||
|
Element::Key => "key",
|
||||||
|
Element::Door => "door",
|
||||||
|
Element::Scroll => "scroll",
|
||||||
|
Element::Passage => "passage",
|
||||||
|
Element::Duplicator => "duplicator",
|
||||||
|
Element::Bomb => "bomb",
|
||||||
|
Element::Energizer => "energizer",
|
||||||
|
Element::Star => "star",
|
||||||
|
Element::Clockwise => "clockwise",
|
||||||
|
Element::Counter => "counter",
|
||||||
|
Element::Bullet => "bullet",
|
||||||
|
Element::Water => "water",
|
||||||
|
Element::Forest => "forest",
|
||||||
|
Element::Solid => "solid",
|
||||||
|
Element::Normal => "normal",
|
||||||
|
Element::Breakable => "breakable",
|
||||||
|
Element::Boulder => "boulder",
|
||||||
|
Element::SliderNs => "sliderns",
|
||||||
|
Element::SliderEw => "sliderew",
|
||||||
|
Element::Fake => "fake",
|
||||||
|
Element::Invisible => "invisible",
|
||||||
|
Element::BlinkWall => "blinkwall",
|
||||||
|
Element::Transporter => "transporter",
|
||||||
|
Element::Line => "line",
|
||||||
|
Element::Ricochet => "ricochet",
|
||||||
|
Element::BlinkRayH => "blink_ray_h",
|
||||||
|
Element::Bear => "bear",
|
||||||
|
Element::Ruffian => "ruffian",
|
||||||
|
Element::Object => "object",
|
||||||
|
Element::Slime => "slime",
|
||||||
|
Element::Shark => "shark",
|
||||||
|
Element::SpinningGun => "spinninggun",
|
||||||
|
Element::Pusher => "pusher",
|
||||||
|
Element::Lion => "lion",
|
||||||
|
Element::Tiger => "tiger",
|
||||||
|
Element::BlinkRayV => "blink_ray_v",
|
||||||
|
Element::Head => "head",
|
||||||
|
Element::Segment => "segment",
|
||||||
|
Element::TextBlue => "text_blue",
|
||||||
|
Element::TextGreen => "text_green",
|
||||||
|
Element::TextCyan => "text_cyan",
|
||||||
|
Element::TextRed => "text_red",
|
||||||
|
Element::TextPurple => "text_purple",
|
||||||
|
Element::TextBrown => "text_brown",
|
||||||
|
Element::TextBlack => "text_black",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the alias for parameter 1 based on element type.
|
||||||
|
pub fn p1_alias(self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Element::BlinkWall => Some("start_time"),
|
||||||
|
Element::Bear => Some("sensitivity"),
|
||||||
|
Element::Ruffian => Some("intelligence"),
|
||||||
|
Element::Object => Some("char"),
|
||||||
|
Element::Shark => Some("intelligence"),
|
||||||
|
Element::SpinningGun => Some("intelligence"),
|
||||||
|
Element::Lion => Some("intelligence"),
|
||||||
|
Element::Tiger => Some("intelligence"),
|
||||||
|
Element::Head => Some("intelligence"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the alias for parameter 2 based on element type.
|
||||||
|
pub fn p2_alias(self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Element::Duplicator => Some("rate"),
|
||||||
|
Element::BlinkWall => Some("period"),
|
||||||
|
Element::Ruffian => Some("resting_time"),
|
||||||
|
Element::Slime => Some("speed"),
|
||||||
|
Element::SpinningGun => Some("firing_rate"),
|
||||||
|
Element::Tiger => Some("firing_rate"),
|
||||||
|
Element::Head => Some("deviance"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the alias for parameter 3 based on element type.
|
||||||
|
pub fn p3_alias(self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
Element::Passage => Some("destination"),
|
||||||
|
Element::SpinningGun => Some("firing_type"),
|
||||||
|
Element::Tiger => Some("firing_type"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the human-readable name for an element ID, or describe unknown elements.
|
||||||
|
pub fn element_name(id: u8) -> String {
|
||||||
|
match Element::from_u8(id) {
|
||||||
|
Some(e) => e.name().to_string(),
|
||||||
|
None => format!("unknown ({})", id),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
mod elements;
|
||||||
mod encoding;
|
mod encoding;
|
||||||
mod error;
|
mod error;
|
||||||
mod parse;
|
mod parse;
|
||||||
|
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};
|
||||||
|
|||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use super::elements::{Element, element_name};
|
||||||
|
use super::parse::{Board, Program, Stat, Tile, World};
|
||||||
|
|
||||||
|
/// Write key = value if value != default.
|
||||||
|
macro_rules! kv {
|
||||||
|
($out:expr, $key:expr, $val:expr, $default:expr) => {
|
||||||
|
if $val != $default {
|
||||||
|
writeln!($out, "{} = {}", $key, $val).unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write key = true if value is true.
|
||||||
|
macro_rules! kv_bool {
|
||||||
|
($out:expr, $key:expr, $val:expr) => {
|
||||||
|
if $val {
|
||||||
|
writeln!($out, "{} = true", $key).unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write key = "value" if value != default.
|
||||||
|
macro_rules! kv_str {
|
||||||
|
($out:expr, $key:expr, $val:expr, $default:expr) => {
|
||||||
|
if $val != $default {
|
||||||
|
writeln!($out, "{} = {:?}", $key, $val).unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a World to its text representation.
|
||||||
|
pub fn world_to_text(world: &World) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
write_world_header(&mut output, world);
|
||||||
|
|
||||||
|
for (i, board) in world.boards.iter().enumerate() {
|
||||||
|
output.push('\n');
|
||||||
|
write_board(&mut output, i, board);
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a standalone Board to its text representation.
|
||||||
|
pub fn board_to_text(board: &Board) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
write_board(&mut output, 0, board);
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_world_header(output: &mut String, world: &World) {
|
||||||
|
output.push_str("[world]\n");
|
||||||
|
writeln!(output, "name = {:?}", world.name).unwrap();
|
||||||
|
|
||||||
|
kv!(output, "health", world.health, 100);
|
||||||
|
kv!(output, "ammo", world.ammo, 0);
|
||||||
|
kv!(output, "gems", world.gems, 0);
|
||||||
|
kv!(output, "torches", world.torches, 0);
|
||||||
|
kv!(output, "score", world.score, 0);
|
||||||
|
|
||||||
|
let key_str = keys_to_string(&world.keys);
|
||||||
|
if !key_str.is_empty() {
|
||||||
|
writeln!(output, "keys = {:?}", key_str).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
kv!(output, "starting_board", world.starting_board, 0);
|
||||||
|
kv_bool!(output, "saved_game", world.locked);
|
||||||
|
|
||||||
|
// Print flags until we've printed all the non-empty ones.
|
||||||
|
// If there are empty ones in between, they will be printed, e.g. ["foo", "", "bar"],
|
||||||
|
// but the empty ones at the end will be omitted.
|
||||||
|
let last_non_empty = world.flags.iter().rposition(|f| !f.is_empty());
|
||||||
|
if let Some(last_idx) = last_non_empty {
|
||||||
|
let flags = &world.flags[..=last_idx];
|
||||||
|
write!(output, "flags = [").unwrap();
|
||||||
|
for (i, flag) in flags.iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
output.push_str(", ");
|
||||||
|
}
|
||||||
|
write!(output, "{:?}", flag).unwrap();
|
||||||
|
}
|
||||||
|
output.push_str("]\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
kv!(output, "torch_cycles", world.torch_cycles, 0);
|
||||||
|
kv!(output, "energizer_cycles", world.energizer_cycles, 0);
|
||||||
|
kv!(output, "time", world.time, 0);
|
||||||
|
kv!(output, "time_ticks", world.time_ticks, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keys_to_string(keys: &[bool; 7]) -> String {
|
||||||
|
let key_chars = ['b', 'g', 'c', 'r', 'p', 'y', 'w'];
|
||||||
|
keys.iter()
|
||||||
|
.zip(key_chars.iter())
|
||||||
|
.filter(|(has_key, _)| **has_key)
|
||||||
|
.map(|(_, c)| *c)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_board(output: &mut String, index: usize, board: &Board) {
|
||||||
|
writeln!(output, "[board {}]", index).unwrap();
|
||||||
|
writeln!(output, "title = {:?}", board.name).unwrap();
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
write_terrain(output, &board.tiles);
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
kv!(output, "shots", board.max_shots, 0);
|
||||||
|
kv_bool!(output, "dark", board.is_dark);
|
||||||
|
kv!(output, "exit_n", board.exit_north, 0);
|
||||||
|
kv!(output, "exit_s", board.exit_south, 0);
|
||||||
|
kv!(output, "exit_e", board.exit_east, 0);
|
||||||
|
kv!(output, "exit_w", board.exit_west, 0);
|
||||||
|
kv_bool!(output, "reenter", board.restart_on_zap);
|
||||||
|
kv!(output, "time_limit", board.time_limit, 0);
|
||||||
|
kv!(output, "enter_x", board.enter_x, 0);
|
||||||
|
kv!(output, "enter_y", board.enter_y, 0);
|
||||||
|
kv_str!(output, "message", &board.message, "");
|
||||||
|
|
||||||
|
for (i, stat) in board.stats.iter().enumerate() {
|
||||||
|
let element = get_element_at(board, stat.x, stat.y);
|
||||||
|
output.push('\n');
|
||||||
|
write_stat(output, i, stat, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_terrain(output: &mut String, tiles: &[Tile]) {
|
||||||
|
// Elements: 60x25 grid, element bytes as 2-digit hex
|
||||||
|
for row in 0..25 {
|
||||||
|
for col in 0..60 {
|
||||||
|
let tile = &tiles[row * 60 + col];
|
||||||
|
write!(output, "{:02x}", tile.element).unwrap();
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push('\n');
|
||||||
|
|
||||||
|
// Colors: 60x25 grid, color bytes as 2-digit hex
|
||||||
|
for row in 0..25 {
|
||||||
|
for col in 0..60 {
|
||||||
|
let tile = &tiles[row * 60 + col];
|
||||||
|
write!(output, "{:02x}", tile.color).unwrap();
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_element_at(board: &Board, 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);
|
||||||
|
board.tiles.get(index).map(|t| t.element)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_stat(output: &mut String, index: usize, stat: &Stat, element: Option<u8>) {
|
||||||
|
// Stat header with element type comment
|
||||||
|
let element_comment = match element {
|
||||||
|
Some(id) => element_name(id),
|
||||||
|
None => "off-board".to_string(),
|
||||||
|
};
|
||||||
|
writeln!(output, "[stat {}] # {}", index, element_comment).unwrap();
|
||||||
|
|
||||||
|
writeln!(output, "x = {}", stat.x).unwrap();
|
||||||
|
writeln!(output, "y = {}", stat.y).unwrap();
|
||||||
|
|
||||||
|
kv!(output, "cycle", stat.cycle, 0);
|
||||||
|
kv!(output, "x_step", stat.x_step, 0);
|
||||||
|
kv!(output, "y_step", stat.y_step, 0);
|
||||||
|
|
||||||
|
if stat.under.element != 0 || stat.under.color != 0 {
|
||||||
|
writeln!(
|
||||||
|
output,
|
||||||
|
"under = ({}, {})",
|
||||||
|
stat.under.element, stat.under.color
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
kv!(output, "follower", stat.follower, -1);
|
||||||
|
kv!(output, "leader", stat.leader, -1);
|
||||||
|
kv!(output, "instruction_pointer", stat.instruction_pointer, 0);
|
||||||
|
|
||||||
|
// Parameters with element-specific aliases
|
||||||
|
let elem = element.and_then(Element::from_u8);
|
||||||
|
write_param(output, stat.p1, "p1", elem.and_then(|e| e.p1_alias()));
|
||||||
|
write_param(output, stat.p2, "p2", elem.and_then(|e| e.p2_alias()));
|
||||||
|
write_param(output, stat.p3, "p3", elem.and_then(|e| e.p3_alias()));
|
||||||
|
|
||||||
|
// Program/code
|
||||||
|
match &stat.program {
|
||||||
|
Program::Own(code) if !code.is_empty() => {
|
||||||
|
output.push_str("code = \"\"\"\n");
|
||||||
|
output.push_str(code);
|
||||||
|
if !code.ends_with('\n') {
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
output.push_str("\"\"\"\n");
|
||||||
|
}
|
||||||
|
Program::Bound(idx) => {
|
||||||
|
writeln!(output, "bind = {}", idx).unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_param(output: &mut String, value: u8, generic_name: &str, alias: Option<&str>) {
|
||||||
|
if value == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let name = alias.unwrap_or(generic_name);
|
||||||
|
writeln!(output, "{} = {}", name, value).unwrap();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user