a button refused from here is not dealt again from here
Row 57, live on rung 10 for two hours: the Pewter City pad was GO ROUTE and nothing else, refused `no route` every 800 brain ms, no button pressed. The dealer asks the cheap question and start asks the real one; the blocked ledger closes the gap for every list except a last resort, which ignores that ledger by design. So GO ROUTE's refusal, which wrote the gym door to the ledger, could not take the button off the pad, and re-stamped the door's window every hold, which kept GO OBJECTIVE's only goal excluded for ever. A `no route` or `precondition` refusal is now recorded with the tile the fly stood on, and the dealer does not deal that button from that tile for the blocked window. It is dealt again the moment the fly stands anywhere else or the window closes. Nothing presses; a pad with nothing runnable is empty and the fly waits.
This commit is contained in:
parent
ab3d4cb6fb
commit
acbc655a23
6 changed files with 157 additions and 2 deletions
|
|
@ -552,6 +552,22 @@ pub trait MacroState: GameState {
|
|||
false
|
||||
}
|
||||
|
||||
/// Whether the macro in `slot` was refused, for want of a route or of its precondition, on
|
||||
/// the tile the fly is standing on now, inside the blocked window (`infra/docs/macros-traps.md`
|
||||
/// row 57).
|
||||
///
|
||||
/// The dealer asks the cheap question and `start` asks the real one, so a button can be dealt
|
||||
/// that its own route search refuses. The blocked ledger usually closes that gap -- the refusal
|
||||
/// writes what it could not reach and the dealer stops offering it -- but not for a list that
|
||||
/// deliberately ignores that ledger: a last resort. Live on rung 10, Pewter City, two hours:
|
||||
/// the pad was `GO ROUTE` and nothing else, refused `no route` every 800 brain ms, because
|
||||
/// `ways`'s last resort ignores the window its own refusal writes. A refusal from *here* is a
|
||||
/// fact about here; the fly standing anywhere else, or the window closing, is what can change
|
||||
/// it. Session state, never checkpointed.
|
||||
fn refused_here(&mut self, _slot: u8) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether `GO ITEM` or `GO NPC` has already arrived at `target` and faced it this session.
|
||||
///
|
||||
/// The other half of the same stall: `GO NPC` walked to the same villager again and again,
|
||||
|
|
@ -907,6 +923,13 @@ pub trait TargetLedger {
|
|||
|
||||
/// Whether `target` on `map` has been arrived at and faced this session.
|
||||
fn reached(&self, map: u8, target: TargetKey) -> bool;
|
||||
|
||||
/// Whether the macro in `slot` was refused standing on `tile` of `map`, inside the window
|
||||
/// ([`MacroState::refused_here`]). Defaulted, so a ledger that never records one refuses
|
||||
/// nothing.
|
||||
fn refused(&self, _map: u8, _slot: u8, _tile: Tile) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Ledgers that have recorded nothing: every target still a candidate.
|
||||
|
|
@ -948,6 +971,9 @@ pub struct Targets {
|
|||
/// stayed shut. The window keeps the loop bounded at one walk per target per window while
|
||||
/// leaving the conversation available.
|
||||
reached: std::collections::BTreeMap<(u8, TargetKey), f64>,
|
||||
/// (map, slot, tile) -> the brain millisecond a macro was refused standing there
|
||||
/// ([`MacroState::refused_here`], row 57). The same window as `blocked`.
|
||||
refused: std::collections::BTreeMap<(u8, u8, Tile), f64>,
|
||||
/// The brain clock of the frame being decided, from [`super::driver::PokemonPalette`].
|
||||
now_ms: f64,
|
||||
/// How long an entry excludes its target, in brain milliseconds.
|
||||
|
|
@ -989,6 +1015,7 @@ impl Targets {
|
|||
blocked: std::collections::BTreeMap::new(),
|
||||
strikes: std::collections::BTreeMap::new(),
|
||||
reached: std::collections::BTreeMap::new(),
|
||||
refused: std::collections::BTreeMap::new(),
|
||||
now_ms: 0.0,
|
||||
window_ms: minutes * MINUTE_MS,
|
||||
}
|
||||
|
|
@ -1006,6 +1033,15 @@ impl Targets {
|
|||
self.window_ms / MINUTE_MS
|
||||
}
|
||||
|
||||
/// Record that the macro in `slot` was refused -- no route, or no precondition -- with the fly
|
||||
/// standing on `tile` of `map` (row 57). Re-recording restarts the window, as `blocked` does.
|
||||
pub fn record_refused(&mut self, map: u8, slot: u8, tile: Tile) {
|
||||
let now = self.now_ms;
|
||||
let window = self.window_ms;
|
||||
self.refused.retain(|_, at| now - *at < window);
|
||||
self.refused.insert((map, slot, tile), now);
|
||||
}
|
||||
|
||||
/// Record a `Blocked` or `Timeout` abort against the target it was aimed at.
|
||||
///
|
||||
/// Re-recording restarts the window, which is the honest reading: the walk failed *again*.
|
||||
|
|
@ -1080,4 +1116,8 @@ impl TargetLedger for Targets {
|
|||
fn reached(&self, map: u8, target: TargetKey) -> bool {
|
||||
self.reached.get(&(map, target)).is_some_and(|at| self.now_ms - *at < self.window_ms)
|
||||
}
|
||||
|
||||
fn refused(&self, map: u8, slot: u8, tile: Tile) -> bool {
|
||||
self.refused.get(&(map, slot, tile)).is_some_and(|at| self.now_ms - *at < self.window_ms)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,6 +220,11 @@ impl PokemonPalette {
|
|||
if let Some((map, tile)) = self.machine.take_pushed() {
|
||||
self.pushed.record(map, tile);
|
||||
}
|
||||
// A refusal from where the fly is standing: that button is not dealt again from this tile
|
||||
// for the window (row 57). The fly moving, or the window closing, deals it again.
|
||||
if let Some((map, slot, tile)) = self.machine.take_refused() {
|
||||
self.targets.record_refused(map, slot, tile);
|
||||
}
|
||||
// A frontier the walk could not reach any of: a fact about this map's ground, with no
|
||||
// window on it (section 12.14).
|
||||
if let Some(map) = self.machine.take_exhausted() {
|
||||
|
|
@ -452,6 +457,7 @@ impl MacroPalette for PokemonPalette {
|
|||
// reach: the fly is about to be standing somewhere else.
|
||||
let _ = self.machine.take_pushed();
|
||||
let _ = self.machine.take_exhausted();
|
||||
let _ = self.machine.take_refused();
|
||||
// The cached palette was dealt for a frame that is being thrown away. Dropping it makes
|
||||
// the next `start` before the next `observe` a nameless refusal, which presses nothing
|
||||
// and reports nothing, rather than a named refusal against a scene that no longer exists.
|
||||
|
|
|
|||
|
|
@ -590,6 +590,10 @@ pub struct MacroMachine {
|
|||
/// from the frame the push is seen, because by the time the next macro starts the fly has been
|
||||
/// walked somewhere else.
|
||||
pushed_tile: Option<(u8, Tile)>,
|
||||
/// A refusal the route search or the precondition made, and where the fly stood for it --
|
||||
/// `(map, slot, tile)`, waiting to be taken into the session's ledger
|
||||
/// ([`super::cartridge::Targets::record_refused`], row 57 of `infra/docs/macros-traps.md`).
|
||||
refused_at: Option<(u8, u8, Tile)>,
|
||||
/// The target a frame-cap `Timeout` spent itself on, and whether the walk ended nearer a goal
|
||||
/// than it began: [`super::cartridge::Targets::record_timeout`]'s two arguments.
|
||||
///
|
||||
|
|
@ -661,6 +665,7 @@ impl MacroMachine {
|
|||
reached: None,
|
||||
exhausted: None,
|
||||
pushed_tile: None,
|
||||
refused_at: None,
|
||||
timed_out: None,
|
||||
resume: VecDeque::new(),
|
||||
pending_talk: None,
|
||||
|
|
@ -689,10 +694,19 @@ impl MacroMachine {
|
|||
machine.outcome = Some((spec.name, MacroAbort::Refused));
|
||||
Err(MacroRefused { slot, reason })
|
||||
};
|
||||
// Where the fly stands for a refusal that is a fact about *here* (row 57): no route from
|
||||
// this tile, or a precondition the dealer and the starter answered differently on it.
|
||||
let here = state.player().map(|player| (player.map, Tile::new(player.x, player.y)));
|
||||
let refused_here = |machine: &mut Self| {
|
||||
if let Some((map, tile)) = here {
|
||||
machine.refused_at = Some((map, slot.0, tile));
|
||||
}
|
||||
};
|
||||
if class(scene) != class(palette.scene) {
|
||||
return refuse(self, Refusal::WrongScene);
|
||||
}
|
||||
if !precondition(spec.kind, state) {
|
||||
refused_here(self);
|
||||
return refuse(self, Refusal::Precondition);
|
||||
}
|
||||
let mut unreachable: Vec<TargetKey> = Vec::new();
|
||||
|
|
@ -712,6 +726,7 @@ impl MacroMachine {
|
|||
self.exhausted = Some(map);
|
||||
}
|
||||
}
|
||||
refused_here(self);
|
||||
return refuse(self, Refusal::NoRoute);
|
||||
};
|
||||
// The map the target was chosen on, so an entry cannot be read back on another map.
|
||||
|
|
@ -883,6 +898,12 @@ impl MacroMachine {
|
|||
self.pushed_tile.take()
|
||||
}
|
||||
|
||||
/// Where the last `no route` or `precondition` refusal happened, taken rather than read
|
||||
/// (row 57).
|
||||
pub fn take_refused(&mut self) -> Option<(u8, u8, Tile)> {
|
||||
self.refused_at.take()
|
||||
}
|
||||
|
||||
/// The frame-cap timeout a walk earned, with whether it ended nearer its goal, taken rather
|
||||
/// than read. The ledger decides what it means.
|
||||
pub fn take_timeout(&mut self) -> Option<(u8, TargetKey, bool)> {
|
||||
|
|
@ -908,6 +929,7 @@ impl MacroMachine {
|
|||
// out of reach either: the fly is about to be somewhere else entirely.
|
||||
self.pushed_tile = None;
|
||||
self.exhausted = None;
|
||||
self.refused_at = None;
|
||||
self.timed_out = None;
|
||||
// A rollback puts the fly somewhere else on the map, so every suspended route is a route
|
||||
// from a tile it is no longer standing on. `take_resume` would refuse them one at a time;
|
||||
|
|
|
|||
|
|
@ -440,7 +440,10 @@ impl Palette {
|
|||
pub fn for_scene(scene: Scene, state: &mut dyn MacroState) -> Self {
|
||||
let mut slots: [Option<MacroSpec>; SLOTS] = [None; SLOTS];
|
||||
for kind in scene_set(scene, state) {
|
||||
if precondition(kind, state) {
|
||||
// A button refused from this very tile inside the window is not dealt again from it
|
||||
// (row 57): the dealer's question is the cheap one, and `start`'s answer to the real
|
||||
// one outranks it until the fly stands somewhere else or the window closes.
|
||||
if precondition(kind, state) && !state.refused_here(kind.slot()) {
|
||||
slots[usize::from(kind.slot())] = Some(MacroSpec::of(kind));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -729,6 +729,10 @@ impl MacroState for World {
|
|||
self.targets.reached(self.map, target)
|
||||
}
|
||||
|
||||
fn refused_here(&mut self, slot: u8) -> bool {
|
||||
self.targets.refused(self.map, slot, self.player)
|
||||
}
|
||||
|
||||
fn objective(&mut self) -> Option<Objective> {
|
||||
self.objective
|
||||
}
|
||||
|
|
@ -786,6 +790,9 @@ fn drive(
|
|||
if let Some(map) = machine.take_exhausted() {
|
||||
world.exhausted.insert(map);
|
||||
}
|
||||
if let Some((map, slot, tile)) = machine.take_refused() {
|
||||
world.targets.record_refused(map, slot, tile);
|
||||
}
|
||||
return Err(refused);
|
||||
}
|
||||
// A walk's cap is its plan's, so the bound here is the ceiling on any macro plus slack.
|
||||
|
|
@ -4874,9 +4881,80 @@ fn a_tile_the_cartridge_pushes_the_fly_off_is_not_a_tile_to_walk_to() {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Row 57: an escorted walk, and where it was escorted from
|
||||
// Row 57: a last resort refused from here is not dealt again from here
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// Pewter City as the live pad found it: the fly fenced into a corner of the town, the
|
||||
/// frontier marked, every person and sign accounted for, the errands paid -- and the only way
|
||||
/// out anything aims at, the gym's door, on the far side of the fence.
|
||||
fn fenced_in_pewter() -> World {
|
||||
let mut world = World::room().at(3, 3);
|
||||
world.map = maps::PEWTER_CITY;
|
||||
world.size = MapSize { width: 20, height: 12 };
|
||||
// The fence: a wall the height of the map, with the fly on the near side of it.
|
||||
for y in 0..12 {
|
||||
world.walls.insert(Tile::new(10, y));
|
||||
}
|
||||
world.warps = vec![Warp { x: 15, y: 3, destination_warp: 0, destination_map: maps::PEWTER_GYM }];
|
||||
world.seen_maps.insert(maps::PEWTER_GYM);
|
||||
world.objective = Some(Objective {
|
||||
map: maps::PEWTER_GYM,
|
||||
tile: None,
|
||||
warp: None,
|
||||
edge: None,
|
||||
target: Some(PlaceKind::Person),
|
||||
});
|
||||
world.areas.insert((Amenity::Mart, maps::PEWTER_CITY));
|
||||
world.areas.insert((Amenity::Center, maps::PEWTER_CITY));
|
||||
world.exhausted.insert(maps::PEWTER_CITY);
|
||||
world
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_way_out_refused_from_here_is_not_dealt_again_from_here() {
|
||||
// Row 57, live on rung 10 for two hours: the pad was `GO ROUTE` alone, `refused` every 800
|
||||
// brain ms, nothing pressed. `ways`'s last resort deliberately ignores the blocked ledger, so
|
||||
// the `no route` refusal -- which writes what it could not reach to that ledger -- could not
|
||||
// take the button off the pad, and the same refusal re-stamped the gym door every hold, which
|
||||
// kept `GO OBJECTIVE`'s only goal excluded for ever.
|
||||
let mut world = fenced_in_pewter();
|
||||
let door = TargetKey::Exit(ExitId::Warp(0));
|
||||
let names = |world: &mut World| -> Vec<&'static str> {
|
||||
let scene = world.scene();
|
||||
plan::plan_for(scene, world).slots.iter().flatten().map(|spec| spec.name).collect()
|
||||
};
|
||||
assert_eq!(names(&mut world), ["GO OBJECTIVE", "GO ROUTE"], "the door, two ways");
|
||||
|
||||
assert_eq!(
|
||||
run(&mut world, MacroKind::GoObjective).map_err(|refused| refused.reason),
|
||||
Err(Refusal::NoRoute)
|
||||
);
|
||||
assert!(world.targets.blocked(world.map, door), "the door is excluded for the window");
|
||||
assert_eq!(names(&mut world), ["GO ROUTE"], "and the last resort still deals it");
|
||||
assert_eq!(
|
||||
run(&mut world, MacroKind::GoRoute).map_err(|refused| refused.reason),
|
||||
Err(Refusal::NoRoute),
|
||||
"the route search agrees with the fence"
|
||||
);
|
||||
|
||||
// The trap: before row 57 the same button was dealt again, from the same tile, on the next
|
||||
// hold, for ever.
|
||||
assert!(
|
||||
names(&mut world).is_empty(),
|
||||
"a button refused from this tile is not dealt again from it: {:?}",
|
||||
names(&mut world)
|
||||
);
|
||||
|
||||
// It is a fact about *here*, not a retirement: standing anywhere else deals it again ...
|
||||
world.player = Tile::new(3, 4);
|
||||
assert_eq!(names(&mut world), ["GO ROUTE"]);
|
||||
// ... and so does the window closing on the same tile.
|
||||
world.player = Tile::new(3, 3);
|
||||
assert!(names(&mut world).is_empty());
|
||||
world.targets.clock(BLOCKED_MINUTES_DEFAULT * 60_000.0 + 1.0);
|
||||
assert!(names(&mut world).contains(&"GO ROUTE"), "{:?}", names(&mut world));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_escorted_walk_walls_the_tile_it_reached_not_the_one_it_set_out_from() {
|
||||
// Row 57's other half. Pewter City's youngster takes the joypad on four tiles by the road
|
||||
|
|
|
|||
|
|
@ -1804,6 +1804,12 @@ impl MacroState for PokeState<'_> {
|
|||
self.targets.blocked(map, target)
|
||||
}
|
||||
|
||||
/// Whether the macro in `slot` was refused on the tile the fly is standing on (row 57).
|
||||
fn refused_here(&mut self, slot: u8) -> bool {
|
||||
let Some(player) = player(self.memory) else { return false };
|
||||
self.targets.refused(player.map, slot, Tile::new(player.x, player.y))
|
||||
}
|
||||
|
||||
/// Whether `GO ITEM` or `GO NPC` has already reached `target` on the map that is loaded.
|
||||
fn reached(&mut self, target: TargetKey) -> bool {
|
||||
let Some(map) = player(self.memory).map(|player| player.map) else { return false };
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue