Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
90ac25d
FRLG: walk open legs through wild tiles a tile at a time
dayfine Nov 1, 2026
8b1de51
FRLG: never plan a hold into a ledge
dayfine Nov 1, 2026
4e7b75f
FRLG: check the overworld before each step, and watch for a battle af…
dayfine Nov 1, 2026
de08068
FRLG: don't watch the step onto a warp for a battle
dayfine Nov 1, 2026
3cc13ad
FRLG: stop on a trainer battle instead of fleeing it
dayfine Nov 1, 2026
6a22c70
FRLG: wait out a missed battle on a leg's last step
dayfine Nov 1, 2026
e6197d9
FRLG: check a warp's fade wasn't a wild battle's
dayfine Nov 1, 2026
d173214
FRLG: after a warp, watch for the opponent's HP box, not the battle t…
dayfine Nov 1, 2026
b4d1976
FRLG: weigh open stops after grass, and stop at ledges moving north
dayfine Nov 1, 2026
0d220a5
FRLG: find the player again at a wall when an open leg is stopped
dayfine Nov 1, 2026
59538a4
FRLG: find the player again without trusting the side they're on
dayfine Nov 1, 2026
c7a9fcb
FRLG: after a trainer stops a leg to a warp, hold on to the warp
dayfine Nov 1, 2026
08829d2
Merge frlg/walk-wild-legs (d1afca983, #118) into frlg/walk-interrupt-…
dayfine Nov 1, 2026
f4be6ff
Merge frlg/walk-wild-legs (e60819ae4, #118) into frlg/walk-interrupt-…
dayfine Nov 1, 2026
5266c44
FRLG: run the re-anchor and ledge route tests on the compiled-in maps
dayfine Nov 1, 2026
a0149c6
Merge frlg/walk-wild-legs (179736609, #118) into frlg/walk-interrupt-…
dayfine Nov 1, 2026
cd30189
Pokemon navigation: drop the tile-at-a-time walk; keep only the stop …
dayfine Nov 1, 2026
df2a10a
FRLG recovery: search from a fixed set of tiles, the leg's own
dayfine Nov 1, 2026
4521543
FRLG recovery: rejoin the route at the stopped leg's end, on the call…
dayfine Nov 1, 2026
8ff8665
Merge frlg/walk-wild-legs (42d7e0aca, #118) into frlg/walk-interrupt-…
dayfine Nov 1, 2026
efb38a1
Merge frlg/walk-wild-legs (91b12a241, #118) into frlg/walk-interrupt-…
dayfine Nov 1, 2026
c0a3ca3
FRLG walker: log the whole route before walking it
dayfine Nov 1, 2026
35e02c3
Merge frlg/walk-wild-legs (79420ed04) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
4c33feb
FRLG walker: log the route in the shared route log lines
dayfine Nov 1, 2026
04f2e27
Merge frlg/walk-wild-legs (0319629f3) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
de84c7d
FRLG recovery tests: check the logged lines in a helper, for the func…
dayfine Nov 1, 2026
215de93
Merge frlg/walk-wild-legs (e5de4f5d9) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
3eb20e4
Merge frlg/walk-wild-legs (fd25e0483, #118: the walker) into frlg/wal…
dayfine Nov 1, 2026
9c70639
FRLG walker: the header says what an interrupted open leg does now
dayfine Nov 1, 2026
0f615e8
Merge frlg/walk-wild-legs (65c83247c) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
be72296
FRLG walker: check each leg to a wall ended at its wall (forest drift…
dayfine Nov 1, 2026
5a85fac
Merge frlg/walk-wild-legs (07bcccadf) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
cc24b58
Merge frlg/walk-wild-legs (85fea6010) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
04a5881
Merge frlg/walk-wild-legs (e77925fef) into frlg/walk-interrupt-recount
dayfine Nov 1, 2026
5736920
Merge frlg/walk-wild-legs (4741f0eca, #118: FRLG maps in the shared P…
dayfine Nov 2, 2026
4e9ec1d
FRLG: re-anchor and recovery test maps in the shared schema
dayfine Nov 2, 2026
41a33de
Merge frlg/walk-wild-legs (#118) into frlg/walk-interrupt-recount; wa…
dayfine Nov 2, 2026
3d248d9
Merge origin/frlg/walk-wild-legs into frlg/walk-interrupt-recount
dayfine Nov 2, 2026
77edbe1
Merge frlg/walk-wild-legs (#118: walker run speed) into frlg/walk-int…
dayfine Nov 2, 2026
a0cbf74
FRLG recovery walk: walk at a given speed
dayfine Nov 2, 2026
f89a7bb
Merge frlg/walk-wild-legs into frlg/walk-interrupt-recount
dayfine Nov 2, 2026
76de5e9
Merge branch 'frlg/walk-wild-legs' into frlg/walk-interrupt-recount
dayfine Nov 3, 2026
89efccf
Merge branch 'frlg/walk-wild-legs' into frlg/walk-interrupt-recount
dayfine Nov 3, 2026
f966a85
Merge frlg/walk-wild-legs (#118) into frlg/walk-interrupt-recount
dayfine Nov 3, 2026
774fc4c
Merge remote-tracking branch 'origin/frlg/walk-wild-legs' into HEAD
dayfine Nov 3, 2026
852cde9
FRLG walker: fight a trainer who spots the player, and go on from whe…
dayfine Nov 3, 2026
b84f91e
Merge origin/frlg/walk-interrupt-recount into frlg/walk-interrupt-rec…
dayfine Nov 3, 2026
c33998b
Merge frlg/walk-wild-legs (6e12b0c86, #118) into frlg/walk-interrupt-…
dayfine Nov 3, 2026
a038b85
FRLG recovery: find the player from the tiles the stop's timing allow…
dayfine Nov 3, 2026
34975cf
Merge frlg/walk-wild-legs (#118: verify mode from #122) into frlg/wal…
dayfine Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
/* Overworld Re-anchor
*
* From: https://github.com/PokemonAutomation/
*
*/

#include <algorithm>
#include <format>
#include "PokemonFRLG_OverworldReanchor.h"

namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonFRLG{


namespace{

constexpr size_t MAX_HOLDS = 3;

Direction opposite(Direction direction){
return (Direction)(((int)direction + 2) % 4);
}

// The way from `from` to `to`, on the same row or column.
Direction towards(const TilePos& from, const TilePos& to){
if (to.x == from.x){
return to.y < from.y ? Direction::NORTH : Direction::SOUTH;
}
return to.x < from.x ? Direction::WEST : Direction::EAST;
}

const MapData* map_named(const std::vector<MapData>& maps, const std::string& name){
for (const MapData& map : maps){
if (map.name == name){
return &map;
}
}
return nullptr;
}

// Whether the player could stand on `position`.
bool standable(const std::vector<MapData>& maps, const MapPosition& position, bool surfing){
const MapData* map = map_named(maps, position.map);
return map != nullptr && in_bounds(*map, position.point)
&& !is_wall(*map, position.point) && object_at(*map, position.point) == nullptr
&& is_water(*map, position.point) == surfing;
}

template <typename Type>
void add_unique(std::vector<Type>& items, const Type& item){
if (std::find(items.begin(), items.end(), item) == items.end()){
items.push_back(item);
}
}

// Where `step` started: `tiles` back from its end, on the same map.
std::optional<MapPosition> walk_back(const std::vector<MapData>& maps, const RouteStep& step){
MapPosition position = step.end;
for (int c = 0; c < step.tiles; c++){
position.point = position.point.next(opposite(step.direction));
if (!standable(maps, position, step.surfing)){
return std::nullopt;
}
}
return position;
}

// Where trainer `object` of `map`, who may see the player on `tile`,
// stands once walked up: next to the player, on the side they came from.
// Each tile a wanderer may stand on is checked on its own, since the side
// depends on it.
void add_trainer_stops(
const MapData& map, size_t object, const MapPosition& tile, std::vector<TrainerStop>& stops
){
const MapObject& trainer = map.objects[object];
const Pokemon::TileBox box = trainer.wander.value_or(
Pokemon::TileBox{.x0 = trainer.x, .y0 = trainer.y, .x1 = trainer.x, .y1 = trainer.y}
);
const int width = box.x1 - box.x0 + 1;
const int count = width * (box.y1 - box.y0 + 1);
for (int c = 0; c < count; c++){
MapObject standing = trainer;
standing.wander.reset();
standing.x = box.x0 + c % width;
standing.y = box.y0 + c / width;
if (trainer_may_see(map, standing, tile.point)){
const TilePos beside = tile.point.next(towards(tile.point, TilePos{standing.x, standing.y}));
add_unique(stops, TrainerStop{.object = object, .at = MapPosition{tile.map, beside}});
}
}
}

// The trainers who may see the player on `tile`, and where each stands
// once walked up.
std::vector<TrainerStop> trainers_seeing(const std::vector<MapData>& maps, const MapPosition& tile){
std::vector<TrainerStop> stops;
const MapData* map = map_named(maps, tile.map);
for (size_t c = 0; map != nullptr && c < map->objects.size(); c++){
if (trainer_may_see(*map, map->objects[c], tile.point)){
add_trainer_stops(*map, c, tile, stops);
}
}
return stops;
}

// `maps` with a trainer who walked up standing on each of `trainers`: they
// stay there after the battle, so the player can't walk through.
std::vector<MapData> with_trainers(const std::vector<MapData>& maps, const std::vector<MapPosition>& trainers){
std::vector<MapData> ret = maps;
for (const MapPosition& at : trainers){
for (MapData& map : ret){
if (map.name == at.map && in_bounds(map, at.point)){
map.tiles.cells[(size_t)at.point.y * map.tiles.width + at.point.x] = TileKind::WALL;
}
}
}
return ret;
}

// Where the player may be, on which of the maps as they may stand (with
// or without a trainer next to them).
struct Hypothesis{
size_t world = 0;
MapPosition at;

bool operator==(const Hypothesis& other) const = default;
};

bool converged(const std::vector<Hypothesis>& positions){
for (const Hypothesis& position : positions){
if (!(position.at == positions[0].at)){
return false;
}
}
return true;
}

// Whether a person stands, or may wander, on a tile `wall` passes after
// `from`. The step rules don't have people, but one there stops the player
// short of the wall.
bool person_on_the_way(
const std::vector<MapData>& world, const MapPosition& from,
bool surfing, Direction direction, const WallHold& wall
){
RouteStep hold;
hold.direction = direction;
hold.tiles = wall.tiles;
hold.surfing = surfing;
const std::vector<MapPosition> tiles = leg_tiles(world, from, hold);
for (size_t c = 1; c < tiles.size(); c++){
const MapData* map = map_named(world, tiles[c].map);
if (map != nullptr && object_at(*map, tiles[c].point) != nullptr){
return true;
}
}
return false;
}

// Holding `direction` from each of `positions`: where they end up, as a leg
// to a wall as long as the longest of them. Nothing if any isn't stopped
// by a wall, or may be stopped short by a person.
std::optional<std::pair<std::vector<Hypothesis>, RouteStep>> hold_all(
const std::vector<std::vector<MapData>>& worlds, const std::vector<Hypothesis>& positions,
bool surfing, Direction direction
){
std::vector<Hypothesis> ends;
RouteStep hold;
hold.direction = direction;
hold.surfing = surfing;
hold.to_wall = true;
hold.facing_after = direction;
for (const Hypothesis& position : positions){
const std::optional<WallHold> wall = hold_to_wall(worlds[position.world], position.at, surfing, direction);
if (!wall || person_on_the_way(worlds[position.world], position.at, surfing, direction, *wall)){
return std::nullopt;
}
add_unique(ends, Hypothesis{position.world, wall->end});
hold.tiles = std::max(hold.tiles, wall->tiles);
hold.wild = hold.wild || wall->wild;
hold.end = wall->end;
}
return std::make_pair(std::move(ends), hold);
}

int total_tiles(const std::vector<RouteStep>& legs){
int total = 0;
for (const RouteStep& leg : legs){
total += leg.tiles;
}
return total;
}

struct Found{
std::vector<RouteStep> holds;
MapPosition at;
};

// Every way of exactly `depth` holds that brings all of `positions` to one
// tile, in a fixed order.
void search(
const std::vector<std::vector<MapData>>& worlds, const std::vector<Hypothesis>& positions, bool surfing,
size_t depth, std::vector<RouteStep>& holds, std::vector<Found>& found
){
for (Direction direction : Pokemon::ALL_DIRECTIONS){
auto next = hold_all(worlds, positions, surfing, direction);
if (!next || next->first == positions || next->second.tiles == 0){
continue;
}
holds.push_back(next->second);
if (converged(next->first)){
if (depth == 1){
found.push_back(Found{holds, next->first[0].at});
}
}else if (depth > 1){
search(worlds, next->first, surfing, depth - 1, holds, found);
}
holds.pop_back();
}
}

// Whether `route`, planned from `at` on other maps, walks the same on
// `world`: each leg to a wall stops at its end, each open leg gets there.
bool walks_same(const std::vector<MapData>& world, MapPosition at, const std::vector<RouteStep>& route){
for (const RouteStep& step : route){
if (step.action == RouteAction::WALK && step.to_wall){
const std::optional<WallHold> hold = hold_to_wall(world, at, step.surfing, step.direction);
if (!hold || !(hold->end == step.end)){
return false;
}
}else if (step.action == RouteAction::WALK){
const std::vector<MapPosition> tiles = leg_tiles(world, at, step);
const size_t expected = (size_t)(step.tiles - (step.warps ? 1 : 0)) + 1;
if (tiles.size() != expected || (!step.warps && !(tiles.back() == step.end))){
return false;
}
}
at = step.end;
}
return true;
}

bool walks_same_everywhere(
const std::vector<std::vector<MapData>>& worlds, const std::vector<Hypothesis>& positions,
const MapPosition& from, const std::vector<RouteStep>& route
){
for (const Hypothesis& position : positions){
if (!walks_same(worlds[position.world], from, route)){
return false;
}
}
return true;
}

// The ways to find the player from all of `positions`: fewest holds first,
// then fewest tiles held. None needed if they are all on one tile.
std::vector<Found> find_player(
const std::vector<std::vector<MapData>>& worlds, const std::vector<Hypothesis>& positions, bool surfing
){
std::vector<Found> found;
if (converged(positions)){
found.push_back(Found{{}, positions[0].at});
}
for (size_t depth = 1; found.empty() && depth <= MAX_HOLDS; depth++){
std::vector<RouteStep> holds;
search(worlds, positions, surfing, depth, holds, found);
std::stable_sort(found.begin(), found.end(), [](const Found& a, const Found& b){
return total_tiles(a.holds) < total_tiles(b.holds);
});
}
return found;
}

}


WalkedTiles walked_near(const RouteStep& step, int estimate, Milliseconds tile_time){
// The estimate is rounded to the nearest tile already.
const int spread = tile_time.count() > 0
? (int)((STOP_TIMING_SLACK + tile_time / 2) / tile_time)
: step.tiles;
return WalkedTiles{
.min = std::clamp(estimate - spread, 0, step.tiles),
.max = std::clamp(estimate + spread, 0, step.tiles),
};
}

WalkedTiles whole_leg(const RouteStep& step){
return WalkedTiles{.min = 0, .max = step.tiles};
}

std::string to_string(const StopCandidate& candidate){
std::string text = to_string(candidate.player);
if (candidate.trainer){
text += std::format(" (stopped by a trainer, now at {})", Pokemon::to_string(candidate.trainer->at.point));
}
return text;
}

std::vector<StopCandidate> stop_candidates(

Check warning on line 299 in SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldReanchor.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::NintendoSwitch::PokemonFRLG::stop_candidates( const std :: vector<MapData> & maps , const std :: optional<MapPosition> & start , const RouteStep & step , WalkedTiles walked): function_lines=29 (advisory above 25, limit 50)

Check warning on line 299 in SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldReanchor.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::NintendoSwitch::PokemonFRLG::stop_candidates( const std :: vector<MapData> & maps , const std :: optional<MapPosition> & start , const RouteStep & step , WalkedTiles walked): function_lines=29 (advisory above 25, limit 50)
const std::vector<MapData>& maps,
const std::optional<MapPosition>& start,
const RouteStep& step,
WalkedTiles walked
){
std::vector<StopCandidate> candidates;
if (step.action != RouteAction::WALK){
return candidates;
}
const std::optional<MapPosition> from = start ? start : walk_back(maps, step);
if (!from){
return candidates;
}
const std::vector<MapPosition> tiles = leg_tiles(maps, *from, step);
const int last = std::min((int)tiles.size() - 1, walked.max);
for (int c = 1; c <= last; c++){
for (const TrainerStop& trainer : trainers_seeing(maps, tiles[c])){
candidates.push_back(StopCandidate{.player = tiles[c], .trainer = trainer});
}
}
if (!candidates.empty() && !step.wild){
return candidates;
}
for (int c = std::max(0, walked.min); c <= last; c++){
add_unique(candidates, StopCandidate{.player = tiles[c], .trainer = std::nullopt});
}
return candidates;
}

std::optional<Reanchor> plan_reanchor(

Check warning on line 329 in SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldReanchor.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::NintendoSwitch::PokemonFRLG::plan_reanchor( const std :: vector<MapData> & maps , const std :: vector<StopCandidate> & candidates , const RouteStep & step , const RouteCosts & costs): function_lines=40 (advisory above 25, limit 50)

Check warning on line 329 in SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldReanchor.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::NintendoSwitch::PokemonFRLG::plan_reanchor( const std :: vector<MapData> & maps , const std :: vector<StopCandidate> & candidates , const RouteStep & step , const RouteCosts & costs): function_lines=40 (advisory above 25, limit 50)
const std::vector<MapData>& maps,
const std::vector<StopCandidate>& candidates,
const RouteStep& step,
const RouteCosts& costs
){
if (candidates.empty() || step.action != RouteAction::WALK){
return std::nullopt;
}

// The maps as they may stand: as they are, and with each trainer who
// may have walked up.
std::vector<std::vector<MapData>> worlds{maps};
std::vector<MapPosition> trainers;
std::vector<Hypothesis> positions;
for (const StopCandidate& candidate : candidates){
size_t world = 0;
if (candidate.trainer){
worlds.push_back(with_trainers(maps, {candidate.trainer->at}));
world = worlds.size() - 1;
add_unique(trainers, candidate.trainer->at);
}
add_unique(positions, Hypothesis{world, candidate.player});
}

const std::vector<MapData> clear_of_trainers = with_trainers(maps, trainers);
for (const Found& at : find_player(worlds, positions, step.surfing)){
const Direction facing = at.holds.empty() ? step.direction : at.holds.back().direction;
// The way back keeps clear of every trainer who may stand in it if
// it can, and must walk the same whether they do or not.
std::optional<std::vector<RouteStep>> back = plan_overworld_route(clear_of_trainers, at.at, RouteGoal{step.end, std::nullopt}, costs, facing);
if (!back && !trainers.empty()){
back = plan_overworld_route(maps, at.at, RouteGoal{step.end, std::nullopt}, costs, facing);
}
if (back && (trainers.empty() || walks_same_everywhere(worlds, positions, at.at, *back))){
return Reanchor{.holds = at.holds, .found = at.at, .back = std::move(*back)};
}
}
return std::nullopt;
}



}
}
}
Loading
Loading