ますたーです。こんにちは。
今回は、預かり屋からタマゴを自動で受け取り、孵化までを自動で行う記事です。ついに全自動記事も第12回。引き続きよろしくお願いします。
なお、Arduino Leonardo自動化の導入・機材構成については導入記事を参考にしてください。
導入記事:【Arduino自動化01】Arduino開発環境の導入
※本ブログに初めてお越しの方は「本ブログについて」もぜひ、ご覧ください。
概要
本記事では、ワイルドエリアの「預かり屋」でタマゴの自動受け取り&自動孵化を紹介します。完全放置で孵化厳選や色厳選作業をできるようにしてみました。
では、目次です。
ソースコードだけほしい方は読み飛ばしてください⇒読み飛ばす
本ブログの紹介環境で自動孵化を作りたかった
始めに、本ブログで「自動孵化」を取り扱おうと思ったきっかけについてです。
今まで、本ブログもまだまだ駆け出しということもあり、世間で公開されているArduino自動化には「無いもの」「新しいもの」をとにかく作ってきました(個人的には、第9回の全自動カレーづくりは大変な力作でした)。そして、本ブログも公開してから3ヶ月が過ぎ、自動化に関わる記事数も10を超え、月間1000アクセスも超えるようになりました。
本ブログでポケモン剣盾のArduino自動化にチャレンジを始めた人もいらっしゃるようなので、せっかくなら「同じ導入環境」で「使えるもの」を増やしたほうが親切かと思い、本ブログでも比較的メジャーな「孵化作業の自動化」に踏み切った次第です。
すなわち、本ブログの「導入記事」での環境で動いていることが(少なくとも筆者は)確認済のプログラムになります。そういった意味では需要があるのかと思います。
まず、孵化厳選で重要なことですが、必ず「ほのおのからだ」「じょうききかん」「マグマのよろい」のいずれかの特性を持つポケモンをパーティーに加えて孵化作業をしましょう。これらの特性をもつポケモンは、手持ちにいるだけで孵化に必要な歩数が半分になります。10匹以上孵化するなら、今から捕まえに行ってもお釣りが来るほど時間効率が変わります。持っていない人は今すぐ捕まえてください。
孵化の歩数を半減できる特性3つのうちいずれかを持っており、かつ、剣盾内(DLCなし)で捕まえやすいポケモンと言えば、タンドン系(タンドン・トロッゴン・セキタンザン)でしょうか。一応、他にもエンジンシティのジムミッションで出会える「ヒトモシ」もいますが、「もらいび」と確率半々なので、DLC無しで確実に捕まえるにはタンドン系が最安定と思います。なお、同特性を持つ剣盾内で出現するポケモンは、ポケモン徹底攻略で検索できますので合わせて参考にしてください。
ところで、剣盾にはDLC含めて「マグマのよろい」を持つポケモン(マグマッグおよびマグカルゴ)は登場しませんでしたね。悲しい…。マグカルゴは私の好きなポケモンの1匹でした。当時、対戦はもちろん、ウルガモスやファイアローが登場してからも孵化のお供に使っており、思い入れ深いポケモンでした。次作では帰ってきてほしいです。
受け取り作業と孵化作業の自動化
自動化に限らず、孵化作業において特筆して準備すべきものは、上記の通り「ほのおのからだ」持ちポケモンだけです。今回の自動化はタマゴを1匹ずつ「受け取り」「孵化」を繰り返すので、1回ごとの試行回数を増やすべく、手持ちポケモンは「ほのおのからだ」持ち1匹のみにしておいてください。
一応、効率的な孵化作業には「自転車」「連続した空きボックス」は必須かと思いますので、本ブログで紹介するプログラムもそれに準じます。なお、本ブログで扱う自動孵化は「ワイルドエリア」の預かり屋を活用します。理由は自転車でくるくると一周できるからです。
プログラムの使い方と修正箇所
本ブログのプログラムを使うにあたっては、下記冒頭箇所を必要に応じて修正してください。プログラム全文は後述します。
#define HATCH_TIME (57)
#define ADV_OFFSET (3)
#define MAX_HATCH_POKEMON (30*9)
#define TOWNMAP_ROW (1)
#define TOWNMAP_COL (0)
#define POKEMON_ROW (0)
#define POKEMON_COL (1)
■1匹の孵化にかかる時間を入力する
計測は難しいですが、ポケモンの孵化歩数などを参考に入力しましょう。数字は「HATCH_TIME」に指定してください。現在デフォルトで入っている57はニャビー(孵化歩数:3840歩?に対して「ほのおのからだ」適用の数字)ですが、かなりざっくりアバウトな数字なので気にせず入力ください。その下の「ADV_OFFSET」は変更しなくて大丈夫です。
#define HATCH_TIME (57)
#define ADV_OFFSET (3)
■空きボックスの数を指定
まず、前述の通り、空きボックス(30匹空白のボックス)を連続して配置してください。その個数を「MAX_HATCH_POKEMON」に適用します。具体的には、例えば、9ボックス連続で空っぽなら、30×9=「270」で指定すればOKです(あるいは計算式で「30*9」でもOK)。
#define MAX_HATCH_POKEMON (30*9)
■Xボタンメニュー画面の位置を入力
Xで開くメニュー画面の「タウンマップ」「ポケモン」のボタンの位置情報を必要に応じて修正(プログラム全文は後述)してください。行(英語でrow)は、上の段が0、下の行が1という決まりで入れています。列(英語でcolumn)は、左端から順番に0, 1, 2, 3, 4になります。
具体的には、タウンマップの行列はそれぞれ「TOWNMAP_ROW」「TOWNMAP_COL」、ポケモンの行列は「POKEMON_ROW」「POKEMON_COL」で指定しているので、これらを修正ください。
よくわからない人は、プログラムではなくゲームのメニュー画面を、下記画像の順番で並び替えればOKです(この順番は、剣盾のシナリオを進めた際のそのままです)。
書き換え作業が終わったら、「タウンマップ」にカーソルをあわせてください。
ソースコード
使う前に、前述の通りXボタンで開くメニュー画面はプログラム上で編集しておき、Xボタンで開いたメニューのカーソルは「タウンマップ」に合う状態にしておいてください。
ワイルドエリアの「預かり屋」にポケモンを預け、手持ちは1匹・自転車に乗った状態で、かつ、「タマゴができている状態」でArduinoを挿すだけで、ひたすら「タマゴを受け取っては孵す」作業を繰り返してくれます。
#include <SwitchControlLibrary.h>
#define HOLDTIME (95)
#define ROW 0
#define COL 1
#define HATCH_TIME (57)
#define ADV_OFFSET (3)
#define MAX_HATCH_POKEMON (30*9)
#define TOWNMAP_ROW (1)
#define TOWNMAP_COL (0)
#define POKEMON_ROW (0)
#define POKEMON_COL (1)
void getEgg(void);
void hatchEgg(void);
int *SetCursor(int *cursor_position, int row, int col);
void SoratobuTaxi(int cursor_position[]);
void SendBox(int* cursor_position, int *hatched_num);
void PushRL(int delay_time_ms);
int PushKey(char* keyname, int holdtime, int delaytime);
void TiltLeftStick(int direction_deg, double power, int holdtime, int delaytime);
void TiltRightStick(int direction_deg, double power, int holdtime, int delaytime);
void TiltStick(int leftdeg, double leftpower, int rightdeg, double rightpower, int holdtime, int delaytime);
void NextDayInCheatMode(void);
int current_cursor[2];
int hatched_num;
void setup() {
for(int i=0;i<7;i++)PushRL(300);
delay(1000);
current_cursor[ROW] = TOWNMAP_ROW;
current_cursor[COL] = TOWNMAP_COL;
hatched_num = 0;
}
void loop() {
getEgg();
hatchEgg();
hatched_num++;
if(hatched_num>0&&hatched_num%5==0){
SendBox(current_cursor, &hatched_num);
}
if(hatched_num > MAX_HATCH_POKEMON)for(;;);
delay(1000);
}
void PushRL(int delay_time_ms){
SwitchControlLibrary().PressButtonR();
SwitchControlLibrary().PressButtonL();
delay(HOLDTIME);
SwitchControlLibrary().ReleaseButtonR();
SwitchControlLibrary().ReleaseButtonL();
delay(delay_time_ms);
return;
}
int PushKey(char* keyname, int holdtime, int delaytime){
if(strlen(keyname)==1){
switch(keyname[0]){
case 'A': case 'a':
SwitchControlLibrary().PressButtonA(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonA(); delay(delaytime);
break;
case 'B': case 'b':
SwitchControlLibrary().PressButtonB(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonB(); delay(delaytime);
break;
case 'X': case 'x':
SwitchControlLibrary().PressButtonX(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonX(); delay(delaytime);
break;
case 'Y': case 'y':
SwitchControlLibrary().PressButtonY(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonY(); delay(delaytime);
break;
case 'L': case 'l':
SwitchControlLibrary().PressButtonL(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonL(); delay(delaytime);
break;
case 'R': case 'r':
SwitchControlLibrary().PressButtonR(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonR(); delay(delaytime);
break;
case 'H': case 'h':
SwitchControlLibrary().PressButtonHome(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonHome(); delay(delaytime);
break;
case '+': case 'p': case 'P':
SwitchControlLibrary().PressButtonPlus(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonPlus(); delay(delaytime);
break;
case '-': case 'm': case 'M':
SwitchControlLibrary().PressButtonMinus(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonMinus(); delay(delaytime);
break;
default:
break;
}
}else if(strlen(keyname)>=2){
switch(keyname[0]){
case 'z': case 'Z':
if(keyname[1]=='R'||keyname[1]=='r'){
SwitchControlLibrary().PressButtonZR(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonZR(); delay(delaytime);
}
if(keyname[1]=='L'||keyname[1]=='l'){
SwitchControlLibrary().PressButtonZL(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonZL(); delay(delaytime);
}
break;
case 'r': case 'R':
SwitchControlLibrary().MoveHat(2); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().MoveHat(8); delay(delaytime);
break;
case 'l': case 'L':
SwitchControlLibrary().MoveHat(6); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().MoveHat(8); delay(delaytime);
break;
case 'u': case 'U':
SwitchControlLibrary().MoveHat(0); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().MoveHat(8); delay(delaytime);
break;
case 'd': case 'D':
SwitchControlLibrary().MoveHat(4); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().MoveHat(8); delay(delaytime);
break;
case 'H': case 'h':
SwitchControlLibrary().PressButtonHome(); delay(holdtime);
if(holdtime>0)SwitchControlLibrary().ReleaseButtonHome(); delay(delaytime);
default:
break;
}
}else{
return -1;
}
return strlen(keyname);
}
void TiltLeftStick(int direction_deg, double power, int holdtime, int delaytime){
double rad = (double)direction_deg*PI/180.0;
int x, y;
x = (double)128*sin(rad)*power;
y = (double)-128*cos(rad)*power;
x += 128; y += 128;
if(x >= 255) x=255; if(x <= 0) x=0;
if(y >= 255) y=255; if(y <= 0) y=0;
SwitchControlLibrary().MoveLeftStick(x,y);
if(holdtime> 0){
delay(holdtime);
SwitchControlLibrary().MoveLeftStick(128,128);
}
if(delaytime>0) delay(delaytime);
return;
}
void TiltRightStick(int direction_deg, double power, int holdtime, int delaytime){
double rad = (double)direction_deg*PI/180.0;
int x, y;
x = (double)128*sin(rad)*power;
y = (double)-128*cos(rad)*power;
x += 128; y += 128;
if(x >= 255) x=255; if(x <= 0) x=0;
if(y >= 255) y=255; if(y <= 0) y=0;
SwitchControlLibrary().MoveRightStick(x,y);
if(holdtime> 0){
delay(holdtime);
SwitchControlLibrary().MoveRightStick(128,128);
}
if(delaytime>0) delay(delaytime);
return;
}
void TiltStick(int leftdeg, double leftpower, int rightdeg, double rightpower, int holdtime, int delaytime){
double rad;
int x, y, rx, ry, lx, ly;
rad = (double)leftdeg*PI/180.0;
x = (double)128*sin(rad)*leftpower;
y = (double)-128*cos(rad)*leftpower;
x += 128; y += 128;
if(x >= 255) x=255; if(x <= 0) x=0;
if(y >= 255) y=255; if(y <= 0) y=0;
lx=x;ly=y;
rad = (double)rightdeg*PI/180.0;
x = (double)128*sin(rad)*rightpower;
y = (double)-128*cos(rad)*rightpower;
x += 128; y += 128;
if(x >= 255) x=255; if(x <= 0) x=0;
if(y >= 255) y=255; if(y <= 0) y=0;
rx=x;ry=y;
SwitchControlLibrary().MoveLeftStick(lx,ly);
SwitchControlLibrary().MoveRightStick(rx,ry);
if(holdtime> 0){
delay(holdtime);
SwitchControlLibrary().MoveLeftStick(128,128);
SwitchControlLibrary().MoveRightStick(128,128);
}
if(delaytime>0) delay(delaytime);
return;
}
void NextDayInCheatMode(){
PushKey("Home", HOLDTIME, 1000);
PushKey("down", HOLDTIME, 105);
PushKey("right", HOLDTIME, 105);
PushKey("right", HOLDTIME, 105);
PushKey("right", HOLDTIME, 105);
PushKey("right", HOLDTIME, 105);
PushKey("right", HOLDTIME, 105);
PushKey("A", HOLDTIME, 1500);
PushKey("down", 1500, 105);
PushKey("right", HOLDTIME, 105);
for(int i=0; i<4; i++){
PushKey("down", HOLDTIME, 55);
}
PushKey("A", HOLDTIME, 500);
PushKey("down", HOLDTIME, 105);
PushKey("down", HOLDTIME, 105);
PushKey("A", HOLDTIME, 500);
PushKey("right", HOLDTIME, 105);
PushKey("right", HOLDTIME, 105);
PushKey("up", HOLDTIME, 105);
PushKey("A", HOLDTIME, 105);
PushKey("A", HOLDTIME, 105);
PushKey("A", HOLDTIME, 105);
PushKey("A", HOLDTIME, 105);
PushKey("Home", HOLDTIME, 2000);
PushKey("A", HOLDTIME, 1000);
return;
}
int *SetCursor(int *cursor_position, int row, int col){
if(cursor_position[COL] < col){
for( ; cursor_position[COL] != col ; cursor_position[COL]++ )PushKey("right", HOLDTIME, 100);
}else if(cursor_position[COL] > col){
for( ; cursor_position[COL] != col ; cursor_position[COL]-- )PushKey("left", HOLDTIME, 100);
}
if(cursor_position[ROW] < row){
for( ; cursor_position[ROW] != row ; cursor_position[ROW]++ )PushKey("down", HOLDTIME, 100);
}else if(cursor_position[ROW] > row){
for( ; cursor_position[ROW] != row ; cursor_position[ROW]-- )PushKey("up", HOLDTIME, 100);
}
return cursor_position;
}
void SoratobuTaxi(int* cursor_position){
PushKey("X", HOLDTIME, 700);
SetCursor(cursor_position, TOWNMAP_ROW, TOWNMAP_COL);
PushKey("A", HOLDTIME, 2700);
PushKey("A", HOLDTIME, 650);
PushKey("A", HOLDTIME, 3800);
return;
}
void getEgg(void){
SoratobuTaxi(current_cursor);
TiltLeftStick( 185, 1.0, 50, 200);
PushKey("L", HOLDTIME, 190);
TiltLeftStick( 0, 1.0, 500, 240);
PushKey("A", HOLDTIME, 420);
PushKey("B", HOLDTIME, 420);
PushKey("A", HOLDTIME, 2800);
PushKey("B", HOLDTIME, 1300);
PushKey("B", HOLDTIME, 1100);
PushKey("B", HOLDTIME, 300);
PushKey("B", HOLDTIME, 1600);
PushKey("B", HOLDTIME, 300);
return;
}
void hatchEgg(){
unsigned long int current_time=0;
unsigned long int start_time=0;
unsigned long int temp_time=0;
TiltLeftStick( 195, 1.0, 50, 200);
PushKey("L", HOLDTIME, 190);
TiltStick(0, 1.0, 0, 0, ADV_OFFSET*1000, 100);
TiltStick(0, 1.0, 90, 1.0, 0, 0);
for( start_time=millis(), current_time=start_time ; current_time - start_time < (HATCH_TIME-ADV_OFFSET)*1000UL ; current_time=millis() );
TiltStick(0, 0, 0, 0, 0, 0);
PushKey("A", HOLDTIME, 18500);
PushKey("B", HOLDTIME, 4200);
return;
}
void SendBox(int* cursor_position, int *hatched_num){
PushKey("X", HOLDTIME, 700);
SetCursor(cursor_position, POKEMON_ROW, POKEMON_COL);
PushKey("A", HOLDTIME, 1500);
PushKey("R", HOLDTIME, 1800);
if(*hatched_num > 30 && *hatched_num %30 == 5){
PushKey("R", HOLDTIME, 800);
}
PushKey("left", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("Y", HOLDTIME, 150);
PushKey("Y", HOLDTIME, 150);
PushKey("A", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("A", HOLDTIME, 150);
PushKey("right", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("down", HOLDTIME, 150);
PushKey("A", HOLDTIME, 800);
PushKey("A", HOLDTIME, 400);
PushKey("B", HOLDTIME, 600);
PushKey("B", HOLDTIME, 2000);
PushKey("B", HOLDTIME, 2000);
PushKey("B", HOLDTIME, 2000);
return;
}
あとがき
今回は、タマゴの自動受取と自動孵化を実装しました。これでよりこのブログも、偉大なる先駆者たちに近づけたかなぁと思います。何よりも自分の手でちゃんと実装ができてよかったです。これからも皆様、応援のほどよろしくお願いいたします!
*
ところで、今回は、予想外のところでプログラミングにつまずきました。
あまり本ブログではプログラムの実装に関する話をする気は無いのですが、今後のプログラミングでも気をつけなきゃなと感じたのでここに「あとがき」として載せます。
Arduinoのint型は2バイトなので32767までしか扱えない
C言語で組み込みやマイコン以外しか触ってこなかった人にとってはかなり衝撃的というか、違和感があるのですが、なんと、int型なのに2バイト(16bit)しかありません。つまり、扱える整数は216=65536通りしかないため、unsigned(マイナスを考えないモード)でも65535までしか扱えないことになります。無論、プログラム中のリテラル(定数)も同様なので、演算の結果intの範囲を超えるとオーバーフローします。
具体的に何に困ったかというと、要するにそのままだと、Arduinoでは「32767ミリ秒以上」待機させることができない(⇒32秒までしか待機できない)ということでした。
…というか10本以上記事を書くだけプログラムしておきながら、今まで一切オーバーフローの可能性に気がつかなかった(=その桁数を扱わずに済んでいた)というのは、奇跡と言わざるを得ません。 一応、ケーススタディとして下記に具体例を載せて解説します。
うまく行かないプログラムソース例:「57秒待機させようとしたらオーバーフローする」
#define WAITTIME (57)
loop(){
unsigned long int current_time=0, start_time=0;
for( start_time=millis(), current_time=start_time ; current_time - start_time < WAITTIME*1000 ; current_time=millis() );
}
このソースでは、「WAITTIME*1000」の計算結果が「57000」になりますが、これが「int型」で演算され、最大値32768以上になることで意図した結果とは異なる動きをしてしまいます。
結論:long intにキャストすればArduinoでも4バイトになる
前述のオーバーフローについて、原因は「文字定数もint型で演算されていること」です。結論、「long型(4バイト)」にキャストしておけば期待した通りに動作します。すなわち、リテラルに接尾辞「L」を付け加えることで解決できます。
改善したプログラムソース例(うまく動く例):「57秒待機するにはlong型にキャストしておく」
#define WAITTIME (57)
loop(){
unsigned long int current_time=0, start_time=0;
for( start_time=millis(), current_time=start_time ; current_time - start_time < WAITTIME*1000UL ; current_time=millis() );
}
上記例では、1000の後に「UL」をつけてunsigned long int型にキャストしています。
接尾辞「L」をつけることで「long型」として扱われます。ちなみに「U」は「unsigned」の意なので、負の数を扱わないことを示します。
*
以上、Arduinoのハードウェア仕様に関する内容にも触れたあとがきでした。この知見も、見る人が見るとためになる内容だと思います。
いずれにしても、皆様のポケモン自動化ライフがよりよいものになることを願っております。
追記:ニャビーの色違い無事に出せました! (1/17 11:27追記)
本ブログを執筆したのが本日1/17の午前4:09。ブログ投稿からわずか6時間、色違いに出会えました。また、このプログラムを動かし続けて7時間以上、ループの破綻なく、無事に動いておりました。これは個人的に安心できました。
なお、今回はわずか189試行で出会えました。これは強運。
改めて、皆様も良い自動化ライフを。
ではではc⌒っ.ω.)っ
導入記事: 【Arduino自動化01】Arduino開発環境の導入
前記事:【Arduino自動化11】5番道路で全自動羽集め【DLC不要】
次記事:【Arduino自動化13】全自動ポケモン逃がし