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.
This commit is contained in:
2026-01-24 14:21:33 -08:00
parent 0419de7ec8
commit c58658f0b5
2 changed files with 53 additions and 26 deletions
+15 -9
View File
@@ -1,3 +1,5 @@
use std::num::NonZero;
use nom::{ use nom::{
IResult, Parser, IResult, Parser,
bytes::complete::{tag, take}, bytes::complete::{tag, take},
@@ -58,10 +60,10 @@ pub struct Board {
pub tiles: Vec<Tile>, pub tiles: Vec<Tile>,
pub max_shots: u8, pub max_shots: u8,
pub is_dark: bool, pub is_dark: bool,
pub exit_north: u8, pub exit_north: Option<NonZero<u8>>,
pub exit_south: u8, pub exit_south: Option<NonZero<u8>>,
pub exit_west: u8, pub exit_west: Option<NonZero<u8>>,
pub exit_east: u8, pub exit_east: Option<NonZero<u8>>,
pub restart_on_zap: bool, pub restart_on_zap: bool,
pub message: String, pub message: String,
pub enter_x: u8, pub enter_x: u8,
@@ -251,8 +253,12 @@ impl Board {
// Read board info // Read board info
let (input, (max_shots, is_dark)) = (le_u8, bool_u8).parse(input)?; 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)?; (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, (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, (enter_x, enter_y, time_limit)) = (le_u8, le_u8, le_i16).parse(input)?;
let (input, _) = take(16usize)(input)?; let (input, _) = take(16usize)(input)?;
@@ -313,10 +319,10 @@ impl Board {
// Board info // Board info
result.push(self.max_shots); result.push(self.max_shots);
result.push_bool(self.is_dark); result.push_bool(self.is_dark);
result.push(self.exit_north); result.push(self.exit_north.map_or(0, |n| n.get()));
result.push(self.exit_south); result.push(self.exit_south.map_or(0, |n| n.get()));
result.push(self.exit_west); result.push(self.exit_west.map_or(0, |n| n.get()));
result.push(self.exit_east); result.push(self.exit_east.map_or(0, |n| n.get()));
result.push_bool(self.restart_on_zap); result.push_bool(self.restart_on_zap);
result.push_string(58, &self.message)?; result.push_string(58, &self.message)?;
result.push(self.enter_x); result.push(self.enter_x);
+38 -17
View File
@@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Write; use std::fmt::Write;
use std::num::NonZero;
use nom::{ use nom::{
IResult, Parser, IResult, Parser,
@@ -103,7 +104,12 @@ fn write_world_header(output: &mut String, world: &World) {
if world.starting_board != 0 { if world.starting_board != 0 {
let comment = board_title_comment(&world.boards, world.starting_board as u8); 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); 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); kv_bool!(output, "dark", board.is_dark);
// Helper to write exit with optional board title comment // Helper to write exit with optional board title comment
fn write_exit(output: &mut String, name: &str, index: u8, boards: Option<&[Board]>) { fn write_exit(
if index != 0 { output: &mut String,
name: &str,
index: Option<NonZero<u8>>,
boards: Option<&[Board]>,
) {
if let Some(n) = index {
let comment = boards let comment = boards
.map(|b| board_title_comment(b, index)) .map(|b| board_title_comment(b, n.get()))
.unwrap_or_default(); .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<usize>), Pars
tiles, tiles,
max_shots: 255, max_shots: 255,
is_dark: false, is_dark: false,
exit_north: 0, exit_north: None,
exit_south: 0, exit_south: None,
exit_west: 0, exit_west: None,
exit_east: 0, exit_east: None,
restart_on_zap: false, restart_on_zap: false,
message: String::new(), message: String::new(),
enter_x: 1, enter_x: 1,
@@ -981,22 +992,22 @@ fn parse_board_section(input: &str) -> Result<(&str, Board, Option<usize>), Pars
} }
"exit_n" => { "exit_n" => {
if let Value::Int(n) = value { if let Value::Int(n) = value {
board.exit_north = n as u8; board.exit_north = NonZero::new(n as u8);
} }
} }
"exit_s" => { "exit_s" => {
if let Value::Int(n) = value { if let Value::Int(n) = value {
board.exit_south = n as u8; board.exit_south = NonZero::new(n as u8);
} }
} }
"exit_e" => { "exit_e" => {
if let Value::Int(n) = value { if let Value::Int(n) = value {
board.exit_east = n as u8; board.exit_east = NonZero::new(n as u8);
} }
} }
"exit_w" => { "exit_w" => {
if let Value::Int(n) = value { if let Value::Int(n) = value {
board.exit_west = n as u8; board.exit_west = NonZero::new(n as u8);
} }
} }
"reenter" => { "reenter" => {
@@ -1063,12 +1074,22 @@ fn remap_board_ref(value: u8, map: &HashMap<usize, usize>) -> u8 {
.unwrap_or(value) .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<NonZero<u8>>, map: &HashMap<usize, usize>) -> Option<NonZero<u8>> {
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. /// Remap board references in a board's exits and passage stats.
fn remap_board_refs(board: &mut Board, map: &HashMap<usize, usize>) { fn remap_board_refs(board: &mut Board, map: &HashMap<usize, usize>) {
board.exit_north = remap_board_ref(board.exit_north, map); board.exit_north = remap_exit_ref(board.exit_north, map);
board.exit_south = remap_board_ref(board.exit_south, map); board.exit_south = remap_exit_ref(board.exit_south, map);
board.exit_east = remap_board_ref(board.exit_east, map); board.exit_east = remap_exit_ref(board.exit_east, map);
board.exit_west = remap_board_ref(board.exit_west, map); board.exit_west = remap_exit_ref(board.exit_west, map);
// Remap passage destinations (p3 for Passage elements) // Remap passage destinations (p3 for Passage elements)
for stat in &mut board.stats { for stat in &mut board.stats {