gave up and limited recursion depth
This commit is contained in:
+16
-10
@@ -112,16 +112,16 @@ impl Board {
|
|||||||
///
|
///
|
||||||
/// With a single grid the draw order is a fixed precedence (no layer walk):
|
/// With a single grid the draw order is a fixed precedence (no layer walk):
|
||||||
///
|
///
|
||||||
/// 1. the player (drawn on top; not part of the grid yet);
|
/// 1. a sensor `Above` the grid whose glyph is visible (drawn on top of the
|
||||||
/// 2. an object at the cell — a solid object always, otherwise the first
|
/// player too);
|
||||||
/// non-transparent non-solid object (`tile != 0`, so invisible objects exist);
|
/// 2. the grid cell's tile — the player or an object — when its glyph is
|
||||||
/// 3. the grid cell `(glyph, arch)` — a solid always draws, a non-solid only when
|
/// visible (`tile != 0`, so invisible objects exist);
|
||||||
/// visible (`tile != 0`);
|
/// 3. a sensor `Below` the grid whose glyph is visible (only reachable
|
||||||
/// 4. a portal at the cell (portals sit on a transparent grid cell);
|
/// because the grid cell drew nothing);
|
||||||
/// 5. a [`decoration`](Board::decorations) at the cell (reached only because the
|
/// 4. a portal at the cell (portals sit on a transparent grid cell), when
|
||||||
/// grid cell was empty);
|
/// its glyph is visible;
|
||||||
/// 6. the [`floor`](Board::floor) glyph, if any;
|
/// 5. the [`floor`](Board::floor) glyph, if any;
|
||||||
/// 7. the canonical black `Empty` glyph.
|
/// 6. the canonical black `Empty` glyph.
|
||||||
///
|
///
|
||||||
/// Panics if out of bounds.
|
/// Panics if out of bounds.
|
||||||
pub fn glyph_at<P: Into<Point>>(&self, p: P) -> Glyph {
|
pub fn glyph_at<P: Into<Point>>(&self, p: P) -> Glyph {
|
||||||
@@ -145,6 +145,12 @@ impl Board {
|
|||||||
return below.scripting.glyph;
|
return below.scripting.glyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A portal at the cell, if its glyph is visible (a `tile = 0` portal is
|
||||||
|
// deliberately invisible, so the floor shows through instead).
|
||||||
|
if let Some(portal) = self.portal_at(p) && portal.glyph.is_visible() {
|
||||||
|
return portal.glyph;
|
||||||
|
}
|
||||||
|
|
||||||
// Otherwise the floor, or the canonical black empty cell.
|
// Otherwise the floor, or the canonical black empty cell.
|
||||||
self.floor.glyph_at(p, self.width).unwrap_or_else(Glyph::transparent)
|
self.floor.glyph_at(p, self.width).unwrap_or_else(Glyph::transparent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ impl BoardSpec {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::BoardSpec;
|
use super::BoardSpec;
|
||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
|
use crate::glyph::Glyph;
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@@ -414,4 +415,59 @@ target_name = "other"
|
|||||||
// The same board converts once the world supplies that script.
|
// The same board converts once the world supplies that script.
|
||||||
assert!(build(&toml_src, &["ghost"]).is_ok());
|
assert!(build(&toml_src, &["ghost"]).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `[[portals]]` block with a default glyph, anchored at `(x, y)`.
|
||||||
|
fn portal_spec(x: usize, y: usize) -> String {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
[[portals]]
|
||||||
|
x = {x}
|
||||||
|
y = {y}
|
||||||
|
name = "east_door"
|
||||||
|
target_board = "room2"
|
||||||
|
target_name = "west_door"
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_portal_without_a_glyph_defaults_to_the_portal_glyph() {
|
||||||
|
let board = build(&spec_toml(3, 1, " @ ", &[PLAYER], &portal_spec(2, 0)), &[])
|
||||||
|
.expect("a portal with no glyph converts");
|
||||||
|
assert_eq!(board.portals[0].glyph, Glyph::portal());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_omitted_portal_glyph_round_trips_as_an_omitted_key() {
|
||||||
|
// The default portal glyph is not serialized — omission is how it round-trips.
|
||||||
|
let spec: BoardSpec =
|
||||||
|
toml::from_str(&spec_toml(3, 1, " @ ", &[PLAYER], &portal_spec(2, 0)))
|
||||||
|
.expect("board table parses");
|
||||||
|
let text = toml::to_string(&spec).unwrap();
|
||||||
|
assert!(!text.contains("glyph"), "default portal glyph is omitted: {text}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glyph_at_draws_a_visible_portal_but_not_a_transparent_one() {
|
||||||
|
// A portal with the default (visible) glyph renders at its cell…
|
||||||
|
let board = build(&spec_toml(3, 1, " @ ", &[PLAYER], &portal_spec(2, 0)), &[])
|
||||||
|
.expect("portals convert");
|
||||||
|
assert_eq!(board.glyph_at((2, 0)), Glyph::portal());
|
||||||
|
|
||||||
|
// …while an explicitly-transparent one (`tile = 0`) stays invisible, so the
|
||||||
|
// floor (here, blank) shows through instead — the sensor-like opt-out.
|
||||||
|
let invisible = r##"
|
||||||
|
[[portals]]
|
||||||
|
x = 2
|
||||||
|
y = 0
|
||||||
|
name = "east_door"
|
||||||
|
target_board = "room2"
|
||||||
|
target_name = "west_door"
|
||||||
|
glyph = { tile = 0, fg = "#000000", bg = "#000000" }
|
||||||
|
"##;
|
||||||
|
let board = build(&spec_toml(3, 1, " @ ", &[PLAYER], invisible), &[])
|
||||||
|
.expect("portals convert");
|
||||||
|
assert_eq!(board.portals[0].glyph, Glyph::transparent());
|
||||||
|
assert!(!board.glyph_at((2, 0)).is_visible());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ pub struct GameState {
|
|||||||
/// position in [`Board::player`](crate::board::Board::player). Scripts mutate
|
/// position in [`Board::player`](crate::board::Board::player). Scripts mutate
|
||||||
/// it via `alter_gems`/`alter_health`/`set_key` and read a snapshot of it.
|
/// it via `alter_gems`/`alter_health`/`set_key` and read a snapshot of it.
|
||||||
pub player: PlayerRef,
|
pub player: PlayerRef,
|
||||||
|
/// How many times we've called resolve_move since the last time this counter
|
||||||
|
/// was reset. Because it's impossible to determine whether a given move + its
|
||||||
|
/// triggered effects will ever terminate, we just cut it at a max limit of
|
||||||
|
/// recursions
|
||||||
|
pub resolve_move_recursion_level: u32
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameState {
|
impl GameState {
|
||||||
@@ -102,6 +107,7 @@ impl GameState {
|
|||||||
speech_bubbles: Vec::new(),
|
speech_bubbles: Vec::new(),
|
||||||
active_scroll: None,
|
active_scroll: None,
|
||||||
board_transition: None,
|
board_transition: None,
|
||||||
|
resolve_move_recursion_level: 0,
|
||||||
player
|
player
|
||||||
};
|
};
|
||||||
state.drain_log();
|
state.drain_log();
|
||||||
@@ -202,6 +208,7 @@ impl GameState {
|
|||||||
// actions immediately so the next object sees the updated board.
|
// actions immediately so the next object sees the updated board.
|
||||||
let ids = self.board().all_ids();
|
let ids = self.board().all_ids();
|
||||||
for id in ids {
|
for id in ids {
|
||||||
|
self.reset_resolve_move_limit(); // Each object gets an independent limit of resolve_move recursions
|
||||||
let actions = self.scripts.run_tick_on(id, secs);
|
let actions = self.scripts.run_tick_on(id, secs);
|
||||||
self.apply_actions(actions);
|
self.apply_actions(actions);
|
||||||
}
|
}
|
||||||
@@ -385,7 +392,18 @@ impl GameState {
|
|||||||
self.run_init();
|
self.run_init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reset_resolve_move_limit(&mut self) {
|
||||||
|
self.resolve_move_recursion_level = 0;
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_move<P: Into<Point>>(&mut self, from: P, dir: Direction) -> bool {
|
fn resolve_move<P: Into<Point>>(&mut self, from: P, dir: Direction) -> bool {
|
||||||
|
// Check the recursion level because we may already be over it:
|
||||||
|
if self.resolve_move_recursion_level > 100 {
|
||||||
|
self.log.push(LogLine::error("resolve_move recursion limit reached"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.resolve_move_recursion_level += 1;
|
||||||
|
|
||||||
let from = from.into();
|
let from = from.into();
|
||||||
// Get the target coords, if they're out of bounds then the move fails.
|
// Get the target coords, if they're out of bounds then the move fails.
|
||||||
let target: Point = dir.from_point(from);
|
let target: Point = dir.from_point(from);
|
||||||
@@ -441,6 +459,7 @@ impl GameState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.reset_resolve_move_limit(); // Starting a new chain of moves, reset the limit
|
||||||
self.resolve_move(player_loc, dir);
|
self.resolve_move(player_loc, dir);
|
||||||
|
|
||||||
// Check if we actually moved
|
// Check if we actually moved
|
||||||
|
|||||||
@@ -107,6 +107,13 @@ impl Glyph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this glyph is the canonical [`portal`](Glyph::portal) glyph;
|
||||||
|
/// keeps it out of saved map files, since omitting the field is how the
|
||||||
|
/// default round-trips.
|
||||||
|
pub fn is_portal_default(&self) -> bool {
|
||||||
|
self == &Self::portal()
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether this glyph draws anything at all.
|
/// Whether this glyph draws anything at all.
|
||||||
///
|
///
|
||||||
/// `false` for the [`transparent`](Glyph::transparent) sentinel, which is how
|
/// `false` for the [`transparent`](Glyph::transparent) sentinel, which is how
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ pub struct Portal {
|
|||||||
pub target_board: String,
|
pub target_board: String,
|
||||||
/// Name of the arrival portal on the target board.
|
/// Name of the arrival portal on the target board.
|
||||||
pub target_name: String,
|
pub target_name: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
/// Visual representation of this portal. Omitted from a map file, it
|
||||||
pub glyph: Option<Glyph>
|
/// defaults to [`Glyph::portal`] — `≡`, black on white — so portals are
|
||||||
|
/// visible unless the author opts out with `tile = 0` (the transparent
|
||||||
|
/// sentinel). Unlike sensors (which are glyphless by default), a portal
|
||||||
|
/// sits on a transparent grid cell and this glyph is what
|
||||||
|
/// [`Board::glyph_at`](crate::board::Board::glyph_at) draws for it.
|
||||||
|
#[serde(default = "Glyph::portal", skip_serializing_if = "Glyph::is_portal_default")]
|
||||||
|
pub glyph: Glyph
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::board::tests::open_board;
|
use crate::board::tests::open_board;
|
||||||
use crate::game::GameState;
|
use crate::game::GameState;
|
||||||
|
use crate::glyph::Glyph;
|
||||||
use crate::portal::Portal;
|
use crate::portal::Portal;
|
||||||
use crate::utils::{Direction, Point};
|
use crate::utils::{Direction, Point};
|
||||||
use crate::world::World;
|
use crate::world::World;
|
||||||
@@ -21,7 +22,8 @@ fn make_board(player: (usize, usize), portals: Vec<Portal>) -> Board {
|
|||||||
board
|
board
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A glyphless portal at `(x, y)` pointing at `target_name` on `target_board`.
|
/// A portal at `(x, y)` pointing at `target_name` on `target_board`, using the
|
||||||
|
/// default portal glyph.
|
||||||
fn portal(
|
fn portal(
|
||||||
x: usize,
|
x: usize,
|
||||||
y: usize,
|
y: usize,
|
||||||
@@ -34,7 +36,7 @@ fn portal(
|
|||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
target_board: target_board.to_string(),
|
target_board: target_board.to_string(),
|
||||||
target_name: target_name.to_string(),
|
target_name: target_name.to_string(),
|
||||||
glyph: None,
|
glyph: Glyph::portal(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user