Skip to content

Commit ccf9b55

Browse files
author
Gin
committed
add LZA box sorter
1 parent 389056c commit ccf9b55

File tree

4 files changed

+415
-0
lines changed

4 files changed

+415
-0
lines changed

SerialPrograms/Source/PokemonLZA/PokemonLZA_Panels.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "PokemonLZA_Settings.h"
1212

1313
// General
14+
#include "Programs/PokemonLZA_BoxSorter.h"
1415
#include "Programs/PokemonLZA_ClothingBuyer.h"
1516
#include "Programs/PokemonLZA_StallBuyer.h"
1617
#include "Programs/PokemonLZA_PostKillCatcher.h"
@@ -70,6 +71,7 @@ std::vector<PanelEntry> PanelListFactory::make_panels() const{
7071
ret.emplace_back(make_multi_switch_program<SelfBoxTrade_Descriptor, SelfBoxTrade>());
7172
ret.emplace_back(make_single_switch_program<PostKillCatcher_Descriptor, PostKillCatcher>());
7273
if (IS_BETA_VERSION){
74+
ret.emplace_back(make_single_switch_program<BoxSorter_Descriptor, BoxSorter>());
7375
}
7476

7577
ret.emplace_back("---- Farming ----");
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
/* LZA Box Sorter
2+
*
3+
* From: https://github.com/PokemonAutomation/
4+
*
5+
*/
6+
7+
8+
#include <array>
9+
#include <map>
10+
#include <optional>
11+
#include <sstream>
12+
#include <algorithm>
13+
#include "Common/Cpp/Exceptions.h"
14+
#include "CommonFramework/Exceptions/OperationFailedException.h"
15+
#include "CommonFramework/ImageTools/ImageBoxes.h"
16+
#include "CommonFramework/ImageTools/ImageStats.h"
17+
#include "CommonFramework/Notifications/ProgramNotifications.h"
18+
#include "CommonFramework/Tools/ErrorDumper.h"
19+
#include "CommonFramework/VideoPipeline/VideoFeed.h"
20+
#include "CommonFramework/ProgramStats/StatsTracking.h"
21+
#include "CommonTools/Async/InferenceRoutines.h"
22+
#include "CommonTools/VisualDetectors/FrozenImageDetector.h"
23+
#include "CommonTools/StartupChecks/StartProgramChecks.h"
24+
#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h"
25+
#include "Pokemon/Pokemon_Strings.h"
26+
#include "Pokemon/Resources/Pokemon_PokemonNames.h"
27+
#include "Pokemon/Pokemon_BoxCursor.h"
28+
#include "Pokemon/Pokemon_CollectedPokemonInfo.h"
29+
#include "PokemonHome/Inference/PokemonHome_BoxGenderDetector.h"
30+
#include "PokemonHome/Inference/PokemonHome_BallReader.h"
31+
#include "PokemonLZA/Inference/Boxes/PokemonLZA_BoxDetection.h"
32+
#include "PokemonLZA/Inference/Boxes/PokemonLZA_BoxInfoDetector.h"
33+
#include "PokemonLZA/Resources/PokemonLZA_AvailablePokemon.h"
34+
#include "PokemonLZA_BoxSorter.h"
35+
36+
namespace PokemonAutomation{
37+
namespace NintendoSwitch{
38+
namespace PokemonLZA{
39+
using namespace Pokemon;
40+
41+
42+
43+
const size_t MAX_BOXES = 32;
44+
45+
BoxSorter_Descriptor::BoxSorter_Descriptor()
46+
: SingleSwitchProgramDescriptor(
47+
"PokemonLZA:BoxSorter",
48+
STRING_POKEMON + " Legends: Z-A", "Box Sorter",
49+
"Programs/PokemonLZA/BoxSorter.html",
50+
"Order boxes of " + STRING_POKEMON + ".",
51+
ProgramControllerClass::StandardController_NoRestrictions,
52+
FeedbackType::REQUIRED,
53+
AllowCommandsWhenRunning::DISABLE_COMMANDS,
54+
{}
55+
)
56+
{}
57+
struct BoxSorter_Descriptor::Stats : public StatsTracker{
58+
Stats()
59+
: pkmn(m_stats["Pokemon"])
60+
, empty(m_stats["Empty Slots"])
61+
, compare(m_stats["Compares"])
62+
, swaps(m_stats["Swaps"])
63+
{
64+
m_display_order.emplace_back(Stat("Pokemon"));
65+
m_display_order.emplace_back(Stat("Empty Slots"));
66+
m_display_order.emplace_back(Stat("Compares"));
67+
m_display_order.emplace_back(Stat("Swaps"));
68+
}
69+
std::atomic<uint64_t>& pkmn;
70+
std::atomic<uint64_t>& empty;
71+
std::atomic<uint64_t>& compare;
72+
std::atomic<uint64_t>& swaps;
73+
};
74+
std::unique_ptr<StatsTracker> BoxSorter_Descriptor::make_stats() const{
75+
return std::unique_ptr<StatsTracker>(new Stats());
76+
}
77+
78+
BoxSorter::BoxSorter()
79+
: NUM_BOXES(
80+
"<b>Number of Boxes to Sort:</b>",
81+
LockMode::LOCK_WHILE_RUNNING,
82+
1, 1, MAX_BOXES
83+
)
84+
, SORT_TABLE(
85+
"<b>Sort Order Rules:</b><br>Sort order rules will be applied top to bottom."
86+
)
87+
, OUTPUT_FILE(
88+
false,
89+
"<b>Output File:</b><br>JSON file basename to catalogue box info found by the program.",
90+
LockMode::LOCK_WHILE_RUNNING,
91+
"box_order",
92+
"box_order"
93+
)
94+
, DRY_RUN(
95+
"<b>Dry Run:</b><br>Catalogue and make sorting plan without execution. Check output at <output_file>.json and <output_file>-sorted.json)",
96+
LockMode::LOCK_WHILE_RUNNING,
97+
false
98+
)
99+
, NOTIFICATIONS({
100+
&NOTIFICATION_PROGRAM_FINISH
101+
})
102+
{
103+
PA_ADD_OPTION(NUM_BOXES); //number of boxes to check and sort
104+
PA_ADD_OPTION(SORT_TABLE);
105+
PA_ADD_OPTION(OUTPUT_FILE);
106+
PA_ADD_OPTION(DRY_RUN);
107+
PA_ADD_OPTION(NOTIFICATIONS);
108+
}
109+
110+
111+
//Move the cursor to the given coordinates, knowing current pos via the cursor struct
112+
[[nodiscard]] BoxCursor move_cursor_to(SingleSwitchProgramEnvironment& env, ProControllerContext& context, const BoxCursor& cur_cursor, const BoxCursor& dest_cursor, bool holding_pokemon = false){
113+
114+
std::ostringstream ss;
115+
ss << "Moving cursor from " << cur_cursor << " to " << dest_cursor;
116+
env.console.log(ss.str());
117+
118+
uint16_t GAME_DELAY = 30;
119+
// TODO: shortest path movement though pages, boxes
120+
for (size_t i = cur_cursor.box; i < dest_cursor.box; ++i){
121+
pbf_press_button(context, BUTTON_R, 10, GAME_DELAY+30);
122+
}
123+
for (size_t i = dest_cursor.box; i < cur_cursor.box; ++i){
124+
pbf_press_button(context, BUTTON_L, 10, GAME_DELAY+30);
125+
}
126+
127+
BoxDetector box_detector(COLOR_RED, &env.console.overlay());
128+
box_detector.move_cursor(env.program_info(), env.console, context, dest_cursor.row+1, dest_cursor.column, holding_pokemon);
129+
130+
return dest_cursor;
131+
}
132+
133+
void print_boxes_data(const std::vector<std::optional<CollectedPokemonInfo>>& boxes_data, SingleSwitchProgramEnvironment& env){
134+
std::ostringstream ss;
135+
for (const std::optional<CollectedPokemonInfo>& pokemon : boxes_data){
136+
ss << pokemon << "\n";
137+
}
138+
env.console.log(ss.str());
139+
}
140+
141+
std::string create_overlay_info(const CollectedPokemonInfo& pokemon, const BoxDexNummberDetector& dex_number_detector){
142+
const std::string& display_name = get_pokemon_name(pokemon.name_slug).display_name();
143+
144+
std::string overlay_log = dex_number_detector.dex_type() == DexType::HYPERSPACE ? "H" : "L";
145+
146+
char dex_str[4];
147+
snprintf(dex_str, sizeof(dex_str), "%03d", dex_number_detector.dex_number());
148+
overlay_log += std::string(dex_str) + " " + display_name;
149+
if(pokemon.gender == StatsHuntGenderFilter::Male){
150+
overlay_log += " " + UNICODE_MALE;
151+
} else if (pokemon.gender == StatsHuntGenderFilter::Female){
152+
overlay_log += " " + UNICODE_FEMALE;
153+
}
154+
if (pokemon.shiny){
155+
overlay_log += " *";
156+
}
157+
if (pokemon.alpha){
158+
overlay_log += " " + UNICODE_ALPHA;
159+
}
160+
return overlay_log;
161+
}
162+
163+
void sort(
164+
SingleSwitchProgramEnvironment& env,
165+
ProControllerContext& context,
166+
std::vector<std::optional<CollectedPokemonInfo>> boxes_data,
167+
std::vector<std::optional<CollectedPokemonInfo>> boxes_sorted,
168+
BoxSorter_Descriptor::Stats& stats,
169+
BoxCursor& cur_cursor
170+
){
171+
env.log("Start sorting...");
172+
env.add_overlay_log("Start Sorting...");
173+
174+
std::ostringstream ss;
175+
// this need to be separated into functions when I will redo the whole thing but I just wanted it to work
176+
177+
// going thru the sorted list one by one and for each one go through the current pokemon layout to find the closest possible match to fill the slot
178+
for (size_t poke_nb_s = 0; poke_nb_s < boxes_sorted.size(); poke_nb_s++){
179+
if (boxes_sorted[poke_nb_s] == std::nullopt){ // we've hit the end of the sorted list.
180+
break;
181+
}
182+
for (size_t poke_nb = poke_nb_s; poke_nb < boxes_data.size(); poke_nb++){
183+
BoxCursor cursor_s(poke_nb_s);
184+
BoxCursor cursor(poke_nb);
185+
186+
// ss << "Comparing " << boxes_data[poke_nb] << " at " << cursor << " to " << boxes_sorted[poke_nb_s] << " at " << cursor_s;
187+
// env.console.log(ss.str());
188+
// ss.str("");
189+
190+
//check for a match and also check if the pokemon is not already in the slot
191+
stats.compare++;
192+
env.update_stats();
193+
if(boxes_sorted[poke_nb_s] == boxes_data[poke_nb] && poke_nb_s == poke_nb){ // Same spot no need to move.
194+
break;
195+
}
196+
if(boxes_sorted[poke_nb_s] == boxes_data[poke_nb]){
197+
ss << "Swapping " << boxes_data[poke_nb] << " at " << cursor << " and " << boxes_sorted[poke_nb_s] << " at " << cursor_s;
198+
env.console.log(ss.str());
199+
ss.str("");
200+
201+
//moving cursor to the pokemon to pick it up
202+
bool holding_pokemon = false;
203+
cur_cursor = move_cursor_to(env, context, cur_cursor, cursor, holding_pokemon);
204+
pbf_press_button(context, BUTTON_Y, 10, 60);
205+
context.wait_for_all_requests();
206+
207+
//moving to destination to place it or swap it
208+
holding_pokemon = true;
209+
cur_cursor = move_cursor_to(env, context, cur_cursor, cursor_s, holding_pokemon);
210+
pbf_press_button(context, BUTTON_Y, 10, 60);
211+
context.wait_for_all_requests();
212+
213+
std::swap(boxes_data[poke_nb_s], boxes_data[poke_nb]);
214+
stats.swaps++;
215+
env.update_stats();
216+
217+
break;
218+
}
219+
}
220+
}
221+
}
222+
223+
void BoxSorter::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){
224+
StartProgramChecks::check_performance_class_wired_or_wireless(context);
225+
226+
std::vector<SortingRule> sort_preferences = SORT_TABLE.preferences();
227+
if (sort_preferences.empty()){
228+
throw UserSetupError(env.console, "At least one sorting method selection needs to be made!");
229+
}
230+
231+
BoxSorter_Descriptor::Stats& stats = env.current_stats<BoxSorter_Descriptor::Stats>();
232+
233+
// vector that will store data for each slot
234+
std::vector<std::optional<CollectedPokemonInfo>> boxes_data;
235+
236+
BoxCursor cur_cursor{static_cast<uint16_t>(NUM_BOXES-1), 0, 0};
237+
238+
VideoOverlaySet video_overlay_set(env.console);
239+
240+
std::ostringstream ss;
241+
242+
uint16_t VIDEO_DELAY = 50;
243+
244+
BoxDetector box_detector(COLOR_RED, &env.console.overlay());
245+
// BoxPageInfoWatcher info_watcher(&env.console.overlay());
246+
// SomethingInBoxCellWatcher non_empty_watcher(COLOR_BLUE, &env.console.overlay());
247+
BoxShinyDetector shiny_detector(COLOR_RED, &env.console.overlay());
248+
BoxAlphaDetector alpha_detector(COLOR_RED, &env.console.overlay());
249+
SomethingInBoxCellDetector non_empty_detector(COLOR_BLUE, &env.console.overlay());
250+
BoxDexNummberDetector dex_number_detector(env.console);
251+
252+
box_detector.make_overlays(video_overlay_set);
253+
shiny_detector.make_overlays(video_overlay_set);
254+
alpha_detector.make_overlays(video_overlay_set);
255+
non_empty_detector.make_overlays(video_overlay_set);
256+
dex_number_detector.make_overlays(video_overlay_set);
257+
258+
//cycle through each box
259+
for (size_t box_idx = 0; box_idx < NUM_BOXES; box_idx++){
260+
if(box_idx != 0){
261+
// Press button R to move to next box
262+
pbf_press_button(context, BUTTON_R, 10, VIDEO_DELAY+100);
263+
}else{
264+
// Moving the cursor to the first slot in the box
265+
box_detector.move_cursor(env.program_info(), env.console, context, 1, 0);
266+
}
267+
268+
context.wait_for_all_requests();
269+
const std::string log_msg = "Checking box " + std::to_string(box_idx+1) + "/" + std::to_string(NUM_BOXES);
270+
env.log(log_msg);
271+
env.console.overlay().add_log(log_msg);
272+
273+
// Box grid to find empty slots (red boxes) and fill boxes_data with value to check or not for pokemon dex number
274+
275+
ss << "\n";
276+
277+
int num_empty_slots = 0;
278+
for (size_t row = 0; row < BOX_ROWS; row++){
279+
for (size_t column = 0; column < BOX_COLS; column++){
280+
box_detector.move_cursor(env.program_info(), env.console, context, row+1, column);
281+
282+
pbf_wait(context, 100ms); // wait some time for the pokemon info to be updated
283+
VideoSnapshot screen = env.console.video().snapshot();
284+
285+
if(non_empty_detector.detect(screen)){
286+
stats.pkmn++;
287+
env.update_stats();
288+
289+
bool dex_number_detected = dex_number_detector.detect(screen);
290+
if (!dex_number_detected){
291+
OperationFailedException::fire(
292+
ErrorReport::SEND_ERROR_REPORT,
293+
"BoxSorting Check Summary: Unable to read a correct dex number, found: " + std::to_string(dex_number_detector.dex_number_when_error()),
294+
env.console
295+
);
296+
}
297+
298+
// XXX TODO: change code to use regional dex, both Lumiose and Hyperspace
299+
int dex_number = dex_number_detector.dex_number();
300+
std::string name_slug;
301+
// env.add_overlay_log(std::to_string(dex_number_detector.dex_type_color_ratio()));
302+
if (dex_number_detector.dex_type() == DexType::HYPERSPACE){
303+
name_slug = HYPERSPACE_DEX_SLUGS()[dex_number-1];
304+
dex_number += LUMIOSE_DEX_SLUGS().size();
305+
} else{
306+
name_slug = LUMIOSE_DEX_SLUGS()[dex_number-1];
307+
}
308+
309+
boxes_data.push_back(
310+
CollectedPokemonInfo{
311+
.preferences = &sort_preferences,
312+
.dex_number = static_cast<uint16_t>(dex_number),
313+
.name_slug = name_slug,
314+
.shiny = shiny_detector.detect(screen),
315+
.alpha = alpha_detector.detect(screen),
316+
}
317+
);
318+
ss << "\u2705 " ; // checkbox
319+
env.add_overlay_log(create_overlay_info(*boxes_data.back(), dex_number_detector));
320+
}else{
321+
stats.empty++;
322+
num_empty_slots++;
323+
env.add_overlay_log("Empty Slot");
324+
env.update_stats();
325+
boxes_data.push_back(std::nullopt); //empty optional to make sorting easier later
326+
ss << "\u274c " ; // "X"
327+
}
328+
}
329+
ss << "\n";
330+
}
331+
env.console.log(ss.str());
332+
env.add_overlay_log("Empty: " + std::to_string(num_empty_slots) + "/30");
333+
ss.str("");
334+
} // end box_idx
335+
336+
// copy boxes data to sort
337+
std::vector<std::optional<CollectedPokemonInfo>> boxes_sorted = boxes_data;
338+
339+
// sorting copy of boxes_data
340+
std::sort(boxes_sorted.begin(), boxes_sorted.end());
341+
342+
env.console.log("Current boxes data :");
343+
const std::string json_path_basename = OUTPUT_FILE;
344+
save_boxes_data_to_json(boxes_data, json_path_basename + ".json");
345+
346+
env.log("Sorted boxes data :");
347+
print_boxes_data(boxes_sorted, env);
348+
save_boxes_data_to_json(boxes_sorted, json_path_basename + "-sorted.json");
349+
350+
if (!DRY_RUN){
351+
sort(env, context, boxes_data, boxes_sorted, stats, cur_cursor);
352+
}
353+
354+
send_program_finished_notification(env, NOTIFICATION_PROGRAM_FINISH);
355+
356+
}
357+
358+
}
359+
}
360+
}

0 commit comments

Comments
 (0)