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):
|
||||
///
|
||||
/// 1. the player (drawn on top; not part of the grid yet);
|
||||
/// 2. an object at the cell — a solid object always, otherwise the first
|
||||
/// non-transparent non-solid object (`tile != 0`, so invisible objects exist);
|
||||
/// 3. the grid cell `(glyph, arch)` — a solid always draws, a non-solid only when
|
||||
/// visible (`tile != 0`);
|
||||
/// 4. a portal at the cell (portals sit on a transparent grid cell);
|
||||
/// 5. a [`decoration`](Board::decorations) at the cell (reached only because the
|
||||
/// grid cell was empty);
|
||||
/// 6. the [`floor`](Board::floor) glyph, if any;
|
||||
/// 7. the canonical black `Empty` glyph.
|
||||
/// 1. a sensor `Above` the grid whose glyph is visible (drawn on top of the
|
||||
/// player too);
|
||||
/// 2. the grid cell's tile — the player or an object — when its glyph is
|
||||
/// visible (`tile != 0`, so invisible objects exist);
|
||||
/// 3. a sensor `Below` the grid whose glyph is visible (only reachable
|
||||
/// because the grid cell drew nothing);
|
||||
/// 4. a portal at the cell (portals sit on a transparent grid cell), when
|
||||
/// its glyph is visible;
|
||||
/// 5. the [`floor`](Board::floor) glyph, if any;
|
||||
/// 6. the canonical black `Empty` glyph.
|
||||
///
|
||||
/// Panics if out of bounds.
|
||||
pub fn glyph_at<P: Into<Point>>(&self, p: P) -> Glyph {
|
||||
@@ -145,6 +145,12 @@ impl Board {
|
||||
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.
|
||||
self.floor.glyph_at(p, self.width).unwrap_or_else(Glyph::transparent)
|
||||
}
|
||||
|
||||
@@ -235,6 +235,7 @@ impl BoardSpec {
|
||||
mod tests {
|
||||
use super::BoardSpec;
|
||||
use crate::board::Board;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::tile::Tile;
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -414,4 +415,59 @@ target_name = "other"
|
||||
// The same board converts once the world supplies that script.
|
||||
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
|
||||
/// it via `alter_gems`/`alter_health`/`set_key` and read a snapshot of it.
|
||||
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 {
|
||||
@@ -102,6 +107,7 @@ impl GameState {
|
||||
speech_bubbles: Vec::new(),
|
||||
active_scroll: None,
|
||||
board_transition: None,
|
||||
resolve_move_recursion_level: 0,
|
||||
player
|
||||
};
|
||||
state.drain_log();
|
||||
@@ -202,6 +208,7 @@ impl GameState {
|
||||
// actions immediately so the next object sees the updated board.
|
||||
let ids = self.board().all_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);
|
||||
self.apply_actions(actions);
|
||||
}
|
||||
@@ -385,7 +392,18 @@ impl GameState {
|
||||
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 {
|
||||
// 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();
|
||||
// Get the target coords, if they're out of bounds then the move fails.
|
||||
let target: Point = dir.from_point(from);
|
||||
@@ -441,6 +459,7 @@ impl GameState {
|
||||
return;
|
||||
}
|
||||
|
||||
self.reset_resolve_move_limit(); // Starting a new chain of moves, reset the limit
|
||||
self.resolve_move(player_loc, dir);
|
||||
|
||||
// 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.
|
||||
///
|
||||
/// `false` for the [`transparent`](Glyph::transparent) sentinel, which is how
|
||||
|
||||
@@ -26,6 +26,12 @@ pub struct Portal {
|
||||
pub target_board: String,
|
||||
/// Name of the arrival portal on the target board.
|
||||
pub target_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub glyph: Option<Glyph>
|
||||
/// Visual representation of this portal. Omitted from a map file, it
|
||||
/// 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::tests::open_board;
|
||||
use crate::game::GameState;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::portal::Portal;
|
||||
use crate::utils::{Direction, Point};
|
||||
use crate::world::World;
|
||||
@@ -21,7 +22,8 @@ fn make_board(player: (usize, usize), portals: Vec<Portal>) -> 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(
|
||||
x: usize,
|
||||
y: usize,
|
||||
@@ -34,7 +36,7 @@ fn portal(
|
||||
name: name.to_string(),
|
||||
target_board: target_board.to_string(),
|
||||
target_name: target_name.to_string(),
|
||||
glyph: None,
|
||||
glyph: Glyph::portal(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user