From c58658f0b5218bc02c7975a7fd46553aaa49dc5d Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Sat, 24 Jan 2026 14:21:33 -0800 Subject: [PATCH] Use NonZero type to represent board edge exits These are the only board indexes in the ZZT file format that cannot reference the title screen; 0 is reserved to mean "no connection". This change encodes that in the type system. --- src/zzt/parse.rs | 24 +++++++++++++-------- src/zzt/text.rs | 55 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/zzt/parse.rs b/src/zzt/parse.rs index 8b8262b..50a2b8e 100644 --- a/src/zzt/parse.rs +++ b/src/zzt/parse.rs @@ -1,3 +1,5 @@ +use std::num::NonZero; + use nom::{ IResult, Parser, bytes::complete::{tag, take}, @@ -58,10 +60,10 @@ pub struct Board { pub tiles: Vec, pub max_shots: u8, pub is_dark: bool, - pub exit_north: u8, - pub exit_south: u8, - pub exit_west: u8, - pub exit_east: u8, + pub exit_north: Option>, + pub exit_south: Option>, + pub exit_west: Option>, + pub exit_east: Option>, pub restart_on_zap: bool, pub message: String, pub enter_x: u8, @@ -251,8 +253,12 @@ impl Board { // Read board info let (input, (max_shots, is_dark)) = (le_u8, bool_u8).parse(input)?; - let (input, (exit_north, exit_south, exit_west, exit_east)) = + let (input, (exit_n, exit_s, exit_w, exit_e)) = (le_u8, le_u8, le_u8, le_u8).parse(input)?; + let exit_north = NonZero::new(exit_n); + let exit_south = NonZero::new(exit_s); + let exit_west = NonZero::new(exit_w); + let exit_east = NonZero::new(exit_e); let (input, (restart_on_zap, message)) = (bool_u8, pstring(58)).parse(input)?; let (input, (enter_x, enter_y, time_limit)) = (le_u8, le_u8, le_i16).parse(input)?; let (input, _) = take(16usize)(input)?; @@ -313,10 +319,10 @@ impl Board { // Board info result.push(self.max_shots); result.push_bool(self.is_dark); - result.push(self.exit_north); - result.push(self.exit_south); - result.push(self.exit_west); - result.push(self.exit_east); + result.push(self.exit_north.map_or(0, |n| n.get())); + result.push(self.exit_south.map_or(0, |n| n.get())); + result.push(self.exit_west.map_or(0, |n| n.get())); + result.push(self.exit_east.map_or(0, |n| n.get())); result.push_bool(self.restart_on_zap); result.push_string(58, &self.message)?; result.push(self.enter_x); diff --git a/src/zzt/text.rs b/src/zzt/text.rs index f8173ee..abf1a0d 100644 --- a/src/zzt/text.rs +++ b/src/zzt/text.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fmt::Write; +use std::num::NonZero; use nom::{ IResult, Parser, @@ -103,7 +104,12 @@ fn write_world_header(output: &mut String, world: &World) { if world.starting_board != 0 { let comment = board_title_comment(&world.boards, world.starting_board as u8); - writeln!(output, "starting_board = {}{}", world.starting_board, comment).unwrap(); + writeln!( + output, + "starting_board = {}{}", + world.starting_board, comment + ) + .unwrap(); } kv_bool!(output, "saved_game", world.locked); @@ -177,12 +183,17 @@ fn write_board_properties(output: &mut String, board: &Board, boards: Option<&[B kv_bool!(output, "dark", board.is_dark); // Helper to write exit with optional board title comment - fn write_exit(output: &mut String, name: &str, index: u8, boards: Option<&[Board]>) { - if index != 0 { + fn write_exit( + output: &mut String, + name: &str, + index: Option>, + boards: Option<&[Board]>, + ) { + if let Some(n) = index { let comment = boards - .map(|b| board_title_comment(b, index)) + .map(|b| board_title_comment(b, n.get())) .unwrap_or_default(); - writeln!(output, "{} = {}{}", name, index, comment).unwrap(); + writeln!(output, "{} = {}{}", name, n, comment).unwrap(); } } @@ -946,10 +957,10 @@ fn parse_board_section(input: &str) -> Result<(&str, Board, Option), Pars tiles, max_shots: 255, is_dark: false, - exit_north: 0, - exit_south: 0, - exit_west: 0, - exit_east: 0, + exit_north: None, + exit_south: None, + exit_west: None, + exit_east: None, restart_on_zap: false, message: String::new(), enter_x: 1, @@ -981,22 +992,22 @@ fn parse_board_section(input: &str) -> Result<(&str, Board, Option), Pars } "exit_n" => { if let Value::Int(n) = value { - board.exit_north = n as u8; + board.exit_north = NonZero::new(n as u8); } } "exit_s" => { if let Value::Int(n) = value { - board.exit_south = n as u8; + board.exit_south = NonZero::new(n as u8); } } "exit_e" => { if let Value::Int(n) = value { - board.exit_east = n as u8; + board.exit_east = NonZero::new(n as u8); } } "exit_w" => { if let Value::Int(n) = value { - board.exit_west = n as u8; + board.exit_west = NonZero::new(n as u8); } } "reenter" => { @@ -1063,12 +1074,22 @@ fn remap_board_ref(value: u8, map: &HashMap) -> u8 { .unwrap_or(value) } +/// Remap an exit reference using the ephemeral ID -> index mapping. +/// Returns the value unchanged if no mapping exists. +fn remap_exit_ref(value: Option>, map: &HashMap) -> Option> { + value.and_then(|n| { + map.get(&(n.get() as usize)) + .and_then(|&idx| NonZero::new(idx as u8)) + .or(Some(n)) + }) +} + /// Remap board references in a board's exits and passage stats. fn remap_board_refs(board: &mut Board, map: &HashMap) { - board.exit_north = remap_board_ref(board.exit_north, map); - board.exit_south = remap_board_ref(board.exit_south, map); - board.exit_east = remap_board_ref(board.exit_east, map); - board.exit_west = remap_board_ref(board.exit_west, map); + board.exit_north = remap_exit_ref(board.exit_north, map); + board.exit_south = remap_exit_ref(board.exit_south, map); + board.exit_east = remap_exit_ref(board.exit_east, map); + board.exit_west = remap_exit_ref(board.exit_west, map); // Remap passage destinations (p3 for Passage elements) for stat in &mut board.stats {