【剣盾S17シングル】保険カイオーガもやれるはず-最終1953位/レート1756

ポケモン剣盾ランクマッチ - シーズン17(シングル)で使用したパーティの構築記事です。

はじめに ~弱点保険カイオーガはもっとやれたはず~

最終: 1953位(レート1756)

f:id:rocky_manobi:20210501112436j:plain:w300

f:id:rocky_manobi:20210502232546p:plain:w300

こんにちは!以前のように精神を病むことなく純粋に真剣勝負をエンジョイできたことに自身の成長を感じております、Rockyです。シーズン17最期の禁伝環境お疲れさまでした。

最終3桁という目標こそ達成できなかったものの、自分としては過去一番の手応えを感じることができたシーズンだったので初めての構築記事を書いてみます。最終日途中まで3桁帯で戦えていたので、弱点保険カイオーガという軸自体はワンチャンあったはずという気持ちを表現できればと思います。

スカーフやチョッキの方が強い?分かっています。分かっていますがまずは聞いてください。ギミックライクなお膳立てが必要な割には高確率でワークしていたと思いますし、何より圧倒的火力で相手を制圧していく楽しさがそこにはありました。竜王戦ルールが終わってしまう今、結果で示せないことが残念でなりません。さようなら、すべての禁止伝説ポケモン

(未来に成長した自分が見てワンチャンすらないよ!って思えたならそれはそれで嬉しい)

↓↓本編↓↓

並びと構築経緯

f:id:rocky_manobi:20210429213633j:plain

普通のスカーフカイオーガを使っていて、サンダー/ラプラス/黒バドレックスあたりのダイマックスに負ける試合や、ラッキーやHDナットレイに簡単に止められてしまう試合が多かったので、それすら貫ける型ってなんだろうと思い至ったのが「 HC弱点保険瞑想カイオーガ 」だった。 試運転において、特に切り返しが決まったときの脳汁ドバドバ感が忘れられず、カイオーガの圧倒的火力で相手をすりつぶすお膳立てをするためだけに構築を組みはじめた。

もう少しだけ真面目に書くと、① ヒートロトムを使って対カイオーガ軸でよく選出されるサンダーやラプラスダイマックスを切らせてそれを受け、ダイマ後に威力の弱い電気技をカイオーガに打たせてすべてを破壊する動き ②先発として悪ウーラオスをはじめとする多くの襷持ちを フェローチェ で上から処した後にカイオーガを展開、狩り残しもフェローチェで刈るという動き、と①②の二つが割とよく決まったので軸とした。

多少浪漫が過ぎるかと感じていたが、弱点保険の採用率の低さ(1%)故にまったく警戒されなかったり、スカーフを意識した立ち回りをしてくるので積みやすかったりととても動きやすかった。禁伝環境による構築/選出バリエーションの乏しさから行動をテンプレ化しやすかったことも確実に追い風で、仮に予想外のポケモンが出てきても大体ダイストリームで場外まで吹き飛んでいった。

ちなみにカイオーガ採用のきっかけは画面を横から見ていた息子(4)が気に入ったから。「クジラさんのしっぽ、焼き肉のお肉みたいだね〜」とのこと。確かに焼きやすそうな形をしている。

構築コンセプト

ラプラスもサンダーもウーラオスが居ない構築は存在しないのでそのあたりをメタりつつ、HC弱点保険瞑想カイオーガを通す

具体的には...

  • スカーフしおふき読みの相手のダイマックスに合わせてダイジェット受けヒートロトムを投げてダイマックスをからした後にオーガを展開して一方的に破壊する
  • 有利対面で瞑想を積んで要塞化&誰にも受からない状態を作る
  • 電磁波やエレキネットのS操作でスカーフ持っていないカイオーガでも上を取れるようにする
  • 初手1vs1交換を成立させた後にカイオーガで1.8匹倒す
  • 水受けポケモンの弱い弱点技を餌に弱点保険を発動させる

などなど。ロマンにあふれている。こころなしかメテオビームテッカグヤを使いたくなる気持ちに近い。

個体紹介

カイオーガ

f:id:rocky_manobi:20210502221946g:plain

努力値: H252 C252 D4
性格: ひかえめ
特性: あめふらし
持ち物: じゃくてんほけん
技構成: めいそう/しおふき/冷凍ビーム/かみなり

本構築のエース。圧倒的火力で破壊できるポケモンはすべて破壊する。破壊できないポケモンは積むなり弱点保険で破壊できるようにする。破壊できるようになったポケモンをすべて破壊する。どんな構築でも必ず選出して他のポケモンで全力で接待する。

サンダーのおかげで多くの構築の電気打点がほぼ特殊なので、めいそう1積みでほぼ要塞になった。また、Sを捨ててHに振っているので積まずとも大抵の等倍または不一致弱点技はダイマックスすると2耐えしてくれるので打ち合えば負けなかった。

弱点保険の主な餌としては、ラプラスフリーズドライやかみなり、ポリゴン2の放電、ダイマか壁下か瞑想積み後限定だがサンダーの電気技、レジエレキの電気技、黒バドレックスのダイソウゲン、ホワイトキュレムフリーズドライなどなど。特にラプラスは瞑想も詰めるし弱点保険も発動させてくれるので良いお客さんだった。

もちろん弱点保険特有の、切り返しから相手のダイマックスポケモンをワンパンする事を楽しむこともできる。なんだかんだこの勝ち筋を追っているときが一番楽しい。

Sがないので2匹は貫けても3匹全ては貫くことが難しいので、一匹潰してから展開するか、フェローチェをスイーパーとして控えさせるようにしていた。

クリアスモッグトリトドンは天敵。凍るまで冷凍ビームを打ち続けて、凍ったらねっとうが来るのでそこで積んで、、、なんやかんや勝ったり負けたりした。数が減ってくれたのでまあ良しとしていた。

フェローチェ

f:id:rocky_manobi:20210502222247g:plain

努力値: A4 C252 S252
性格: せっかち
特性: ビーストブースト
持ち物: きあいのタスキ
技構成: インファイト/フェイント/冷凍ビーム/エレキネット

安定の先発&スイーパー要因。死ぬほど初手に呼ぶ襷悪ウーラオスインファイト-フェイントで処したい一方で、トリプルアクセルをゴツメ静電気サンダーに受けられるとやる気が無くなってしまうので両刀とした。先発させることが多いので安定感を重視して氷枠は冷凍ビーム。火力は物足りないがH振りサンダーであれば確定2発。羽休め連打されたら凍るまで打ち続ける覚悟で採用した。

死に際にエレキネットでSを下げて、後続のカイオーガが上を取れるようにする動きも強かった。スカーフウオノラゴンの上を取れるもの大きい。フェーローチェ自体今回初めて使ってみたのだけど、HP1で引いても腐ることが少ないのはとても良い。

余談

環境に最速フェローチェが少ないからか、カイリューが対面からダイジェットを打ってくることが多かった。当然抜かれないので冷凍ビーム二発で鎮めてEasyWinを貰える試合がいくつかあったのも印象的。と、たまに黒バドレックスが選出されないことがあったのはこいつが地獄突きを搭載していると思ってくれたおかげだろうか。

ヒートロトム

f:id:rocky_manobi:20210502222615g:plain

努力値: H252 C114 D76 S68
性格: おだやか
特性: ふゆう
持ち物: オボンのみ
技構成: ひかりのかべ/オーバーヒート/ボルトチェンジ/電磁波

光の壁ヒートロトム。どこかからパクった珠サンダー意識の耐久配分がたまたまラプラスのセンリツ+壁下でストリームを耐え、ザシアンのじゃれつくも2耐えしてくれたので採用。MVP級に活躍してくれたが、もっと耐久に振っても良かったかもしれない。 ラプラス、サンダーのダイマックスをいなして起点化したり、禁伝を壁や電磁波でごまかす役割を担ってくれた。当然ザシアンやゴリランダーに対する引き先としても活躍。インファイトを持っているザシアンに後出しすると何もできないが、その場合は電気打点がないことが確定するので裏からカイオーガを出してダイストリームを打った。

ナットレイ対面で電磁波を打って裏のポケモンに指したりもした。相手のパーティにウオノラゴンが居ると途端に選出しづらくなり、かつ最終日に増殖傾向にあったので辛かった。毎シーズンの事らしいけれどそんなの知らなかったよ。

[おまけ] ラプラスとサンダーのダイマックスのいなし方

VSラプラス

キョダイセンリツに対して受け出し、次のターンのストリームを壁で耐える。その後電磁波を打って退場。死に出しでカイオーガを出して上から瞑想を打つと、カミナリかフリーズドライが飛んでくるのでC3D1段階上昇のカイオーガが誕生して試合が決まる。痺れを引いたらありがたくもう一個めいそうを積む。ラプラスダイマックスを切られた試合は一度も負けなかった。ラプラスは弱点保険持ちが多いのでボルトチェンジは打ってはいけない。

別のラプラスダイマックスのいなし方として、カイオーガで上から瞑想を2回積むことでセンリツとダイサンダーを受けて弱点保険を発動させる方法もあるが、エレキフィールドを張られてしまうと裏から出てきたザシアンのワイルドボルトカイオーガが沈んでしまうのでヒトムに引く運用がより強かった。

VSサンダー

ダイジェットに合わせてヒトムに引く。珠ダイジェットでも2耐えするので次のターンに壁を貼ってオボン発動。3ターン目のダイマ技の裏からボルトチェンジカイオーガを対面させる。 

ナットレイ

f:id:rocky_manobi:20210502223000g:plain

努力値: H252 B4 D252 (最遅)
性格: ずぶとい
特性: 鉄の棘
持ち物: たべのこし
技構成: てっぺき/ボディプレス/やどりぎのタネ/ジャイロボール

ここから補完枠。まずは相手のナットレイがA振りパワーウィップ採用なのかどうかでカイオーガの動かし方が変わるのが面倒くさかったので、ナットレイに強い鉄壁ボディプレス搭載のナットレイを採用した。そもそもカイオーガヒートロトムと相性の良い受けポケモンでありつつ、ナットレイ以外の受けポケモンもある程度ごまかせるのでまぁ必然。

なんか柔らかいなと思ったらHBではなくHDに振っていたことに構築記事を書いている段階で気づいた。その上で正確はB補正。まぁ特殊環境とも言えたのでHDで良かったといえば良かったのだけど、そういうとこやで!

雨下で鉄壁を積むとダルマのフレアドライブも耐えるので強かった。

サンダー

f:id:rocky_manobi:20210502222943g:plain

努力値: H252 B4 D252
性格: おだやか
特性: 静電気
持ち物: ゴツゴツメット
技構成: ほうでん/かいでんぱ/みがわり/はねやすめ

2戦連続でスカーフ水ウーラオスにボコボコにされて発狂したものの解決策がおもいつかなかったので、ザシアンにも運ゲーを仕掛けられるゴツメ静電気サンダーを採用した。調整が面倒だったので特に意味もなく雪原環境で愛用していた特殊アタッカーや受けポケを詰ませるHD型を使った。今思うと本気で意味がわからない。そういうとこやで。

この採用理由なら珠カイリューやらドラパルトやらにしておけば色々解決したかもしれない。サンダーいるよりラプラス出しやすいよね。ガマゲロゲとかも見られるよね。 あ〜、急に後悔が押し寄せてきた。あ〜〜〜あ〜〜〜〜。試したい。でも試せない。あ〜〜〜〜。

ランドロス

f:id:rocky_manobi:20210502222916g:plain

努力値: H4 A252 S252
性格: ようき
特性: 威嚇
持ち物: こだわりスカーフ
技構成: ばかぢから/じしん/とんぼがえり/いわなだれ

物理受けまたは特殊アタッカーの選出を誘導できるという願いも込めて地面枠ランドロス。ホウオウ入に投げる、ムゲンダイナ入に先発させてヤンキープレイングしてワンチャンを狙う、レジエレキとザシアンが両採用されている構築に投げる、以外は見せポケ。

カイオーガがいるおかげであまりスカーフがケアされず、出した試合は結構仕事してくれた。選出すると試合の流れがサイクル戦になりがちで楽しかったのも○。

立ち回り

ダイマックスはカイオーガに切りたいので他のポケモンダイマックスせずに戦う必要があるのが最も癖のあるところ。どう考えても臨機応変ダイマックスできる方が強い。コンセプトなので仕方ないと割り切る。

戦術としては以下再掲。

  • スカーフしおふき読みの相手のダイマックスに合わせてダイジェット受けヒートロトムを投げてダイマックスをからした後にオーガを展開して一方的に破壊する
  • 有利対面で瞑想を積んで要塞化&誰にも受からない状態を作る
  • 電磁波やエレキネットのS操作でスカーフ持っていないカイオーガでも上を取れるようにする
  • 初手1vs1交換を成立させた後にカイオーガで1.8匹倒す
  • 水受けポケモンの弱い弱点技を餌に弱点保険を発動させる

選出

フェローチェ/ヒートロトム(orナットレイ)/カイオーガ

基本選出、というかだいたいこれ。先発のフェローチェが1vs1交換や死に際のエレキネットでs操作したあとにカイオーガを繰り出す。

相手の先発がサンダーの場合は冷凍ビームを打つ。居座りならダイジェットを打ってくるので二撃目に合わせてロトムに引いて壁を展開する。それ以外は適当。打点がない場合はとりあえずエレキネットでSを下げたり、ヒトムに引けるなら引く。

初手襷がマンムーっぽい場合はヒトムではなくナットレイを投げる必要がある。

トリトドンがいる場合もナットレイを投げてお茶を濁した。決着が着かないので絶対に数的有利を取られてはいけない。

サンダー、カイオーガナットレイ

受けよりのパーティに選出。カイオーガの火力で数的有利を取れれば身代わり怪電波サンダーか鉄壁ナットレイで勝ち。

ウーラオスが水に見えたときもこの選出をした。判別は水アタッカーの有無の他に「その構築、黒バドレックス来たらウーラオス以外でどう見るの?」の回答があれば水という感じで判断していた。大体当たった。いや、そもそも大体が悪だったし。

ランドロスヒートロトムorナットレイカイオーガ

ムゲンダイナ入に選出。その心は個別紹介にて述べた。苦しい。

重いポケモン

伝説以外ではガマゲロゲトリトドンラグラージあたりか辛かった。

初手すいすい珠ガマゲロゲに勝ったことはない。せめてカイオーガ対面でダイアースではなくダイソウゲンを撃ってくれまいか。

トリトドンは早い段階で数的有利を取ることに全力を尽して、ナットレイカイオーガで誤魔化していた。死ぬほど重い。あくび持っているやつが居たら絶対勝てないと思っていたけど当たらなかった。

ラグラージはこちら視点ではめちゃくちゃ重いのに選出されないこともあって不思議に思っていた。全面に押し出してステロあくびされるだけで崩壊したのに。未だにわからない。

また、カバルドンは殆ど出てこないけど稀に初手に来てめっちゃ困ってなんどか負けた。スカーフカイオーガだったらどうするつもりやったん?

禁止伝説別の立ち回り

対ザシアン

ヒトムにすべてがかかっている。一発耐えてオーバーヒートで鎮めるか電磁波を入れられたら勝ち。ヒトムを選出しづらいときはサンダーを選出し、フェローチェに打ってくる電光石火に合わせて引いて、51%勝負を仕掛ける。 それ以外はエレキフィールドさえなければカイオーガが一撃耐えてダイストリームで処せるので、そういうシチュエーションに持っていくことを意識した。

構築としてラプザシの試合は一つも落とせない覚悟で望んでいた。フェローチェ、ヒトム、カイオーガの基本選出ができる構築であれば8割以上は勝っていた。終盤にいくにつれて減ってしまったのが残念でならない。

ダイマックス枯らしてくる身代わりザシアンは嫌い。

カイオーガ

どんな型であってもカイオーガで基本的に殴り勝てる。お供のサンダーとナットレイフェローチェとヒトムで誤魔化していた。これも7割位は拾えていた。

対バドレックス(黒)

C1段階上昇までならダイソウゲンを受けてからのダイストリームで切り替えせる。対面ではスカーフを警戒してダイマックスを切ってくれるが、ダイマ状態で対面すると身代わり連打でダイマックスを枯らされてしまうので勝てない。

5割強程度。

ネクロズマ

カイオーガが弱点をつかずとも純粋に殴り勝てる。雨が降るので回復技で詰められることもなく、ヒトムで電磁波を入れてもよし、鉄壁ナットレイで受けてもよしと、構築としては驚異に感じてはいなかった。ただ何故かトリトドンガマゲロゲと一緒にいる構築とよくあたって処理に困った。

対ムゲンダイナ

致命的に弱い上にこちらの構築をみるとだいたい初手に出してくる。わかる。 勝率25%くらいで拾えれば良いと思って戦っていた。

相手が受け構築の場合はスカーフランドロスを先発させる。だいたいエアームドに引いてくるので合わせてカイオーガに交換して、瞑想を積んでなんとかする。先に積んだ状態ならラッキーでも潰せる。スカーフやメテオビーム型であった場合は天災だと思って諦める。そんなに当たらなかった。

対ゼルネアス

致命的に勝てない。数が減ってくれて本当に助かった。ヒトムで壁を貼って、HDナットレイカイオーガで誤魔化すことしかできない。何故か選出されないこともあって驚いたりもした。スカーフカイオーガだと仮定すると見え方が変わる?わからない。

2割も勝てなかった。

ディアルガ

ナットレイでもヒートロトムでも後出しできることが多く安定して戦えた。電磁波が入っていればカイオーガがめいそうを積んで勝ち。フェローチェが後ろにいれば適当にダイストリームなりを打っていれば勝ち。

イベルタル

カイオーガと対面するとスカーフを警戒してダイジェットを積みたがるのでウォール、ストリーム、ウォール(orストリーム) で一応戦える。ダイマックスの嵐を超えればサンダーやナットレイでなんとかできる...のだけど、が、読み違えて死ぬ試合多数。フェローチェを先発させづらくなるのが嫌だった。

勝率は4割程度。

対ホウオウ

ヒトムで電磁波。カイオーガで殴る。ラッキーがいる場合は必ず引いてくるので対面ではめいそうを詰む。ランドロスも投げた。

対ホワイトキュレム

4割くらい

その他
  • 対ルギア:あたった試合はすべて怪電波サンダーで詰ませた。十中八九弱保なので電気技は裏のポケモンにすら打ってはいけない。全然いなかった。負けた記憶もない。
  • ゼクロム:先にダイマを切らせれば勝ち。切っていたら負け。3割。しかたない。
  • 対レシラム:記憶にございません。格好いいよねレシラム。
  • パルキア:記憶にございません。別に格好良くもないよね。

あとがき

最後まで読んでいただきありがとうございました。 記事を書く中でいくつか気づきがあったので、最後にそれを共有して終わります。

構築記事を書く前は僕のプレイングと選出が戦犯だと思っていましたが、改めてまとめてみると補完枠のチョイスが致命的に雑なことに気づきました(遅くね?)。「なぜカイオーガではなくフェローチェから入った!!!」「なぜ襷が予想される相手にダイアイスを打たなかった!!!」などと吠える前に、補完枠をもっとちゃんと調整するなりしていれば結果は上振れたことでしょう。パット見楽しいこと(=対戦)だけをやっていてはいけない。

改めて、軸はイケていたんだと思います(少なくとも今の自分にはそう思えます)。また、禁伝環境による構築/選出バリエーションの乏しさから行動をテンプレ化しやすかったことは、(初代以来)8世代復帰勢の自分にとっても、この構築にとっても確実に追い風でした。先程久々に冠環境で対戦をしたら相手の動きが全く読めない完全なる別ゲームになっていて、それだけに今回結果を出せなかったことが悔しいです。

4桁の構築記事に公共的価値なんてないなどと格好つけている場合ではありませんでした。次また真剣にランクマッチに潜ることがあれば結果に関わらず構築記事は必ず書こうと思います。いや、むしろシーズン中に書いてみるTRYをしたいと思います。3桁目指す勢のみなさんも一緒にやりましょう。

3桁以内が目標ですと言いながら決まったときの脳汁が採用理由というのがこの構築記事最大のツッコミどころなので、次は結果でその辺りをごまかせるようにやっていきたいと思います。

それでは。

追記FAQ

めいそうカイオーガ使うならゼルネアス使えば?

並びを見た段階で積んでくるのが見え見えで、ザシアン辛い時点で流石に使う気になれませんでした。それからどうやってダイマックスアドベンチャーでゼルネアスに勝つのかを教えてください。ゲットできないです。

スマホでSwitchを操作できるようにして、隙間時間でポケモンマスターを目指す

LAPRAS夏の自由研究リレー - 8/24枠の記事です。 自由研究リレーとはその名の通り、LAPRASのメンバーが自由研究したことを次々にアウトプットしていくというもので、すでに沢山の記事が公開されています。ぜひご覧ください。

ちなみに当エントリは自由研究と言いつつも「趣味で作った作品の開発ログ + 申し訳程度の技術Tips」という構成になっています。小学生や中学生の自由研究においても、特に何かを研究をするわけでもなくただ単に工作だけしてお茶を濁すというのはよくある話です。あなたのお友達にもいたはずです。このエントリもそんな工作枠として受け入れられることを祈っています。

それでは本編です。

f:id:rocky_manobi:20200824143410p:plain

背景

2019.11.15にポケモン最新作「ソード&シールド」が発売された。自分は金銀以降まったくプレイしていなかったのだが、弊社CTOの @showwin がこの日のために休みをとっていたのを見て試しに買ってみたら、見事に対戦沼にはまってしまった。やるからには当然勝ちたいという欲望が湧いてくるのだが、ここ20年でポケモンは数もシステムも様変わりしており、もう自分の知っているポケモンではなくなっていた。このギャップを埋めるためにはとにかく多く対戦経験を積む必要があった。

Switchよりも手軽に、コッソリとポケモンバトルができる状態を作る

対戦経験を積むといっても時間は無限にあるわけではない。仕事もあれば子供もいる。 隙間時間でササッとポケモンができれば、ながら対戦ができれば、もっと効率よく場数を踏めると考えた。

そのために以下の課題を解決する必要があった。

  • スマホは余裕でいじれるがSwitch本体の操作は無理というシチュエーションが多い
    • 最近の子供の寝かしつけはほぼ隣で横たわっているだけで、光さえ枕で遮断すればスマホは問題ない
  • Switchをやっていると完全にゲームをやっているとバレて無邪気な攻撃をうける
  • Switchをやっていないときでも、無秩序に攻撃が飛んでくることがある
  • 攻撃をうけると一時的に画面を見られない状態になる
  • 子供のオムツ関連事故が発生したときにも、一時的に画面を見られない時間が生じる
  • ながらでやったとしても、選出や立ち回り、ダメージ感覚はあとから振り返りたい
  • 30過ぎたおじさんが満員電車でSwitchでポケモンをやっていたら物理的にも精神的にも迷惑をかけそうだった (これは通勤自体が消滅したので解決した
  • 電車の中で電波が不安定になった時に対戦切断扱いになるのが怖かった (これは通勤自体が消滅したので解決した

Switchよりも手軽に、コッソリとポケモンバトルができる状態を作ることでこれを解決していくことにする。

作ったもの

というわけで出来上がったのが「ポケモン剣盾のランクマッチにスマホで潜れる装置」だ。これを使えば子供の寝かしつけ時や、キッズランド等での付き添い時、電車での移動中、駐車場で買い物待機をしている間にもコッソリとポケモンの修行ができる。 普通にポケモン対戦をすることができる機能に加え、テキスト読み上げやメッセージログ表示など、ながら作業をしていても情報を逃すことがないようなアシスト機能を備えている。AirPods等のイヤホンをつけて使うことを想定している。

特徴

  • 手軽
    • スマホのサイズと操作性
    • 操作手順の削減
    • ずっと集中していなくても重要な情報を落とさない情報表示
  • コッソリ
    • スマホは多用途なのでバレない
    • イヤホンをつけて戦況を音声読み上げで把握するなども可能
  • その他
    • あとで振り返ることができるための、選出、使用した技、入ったダメージ等のロギング

仕組み

仕組みを簡単に説明すると以下のようになる。詳しいやり方は「作り方」を見ていただきたい。

  • スマホで操作をするとインターネット経由でPCに繋げているSwitchにコマンドが送られ、実行される
  • Switchの映像をキャプチャボードを使ってPC側で読み取り、しかるべき解析をしたのちに要所の画像をスマホに送って表示する

基本機能

基本的な画面構成は、選出、行動選択、その他の3つ。順に紹介していく。

f:id:rocky_manobi:20200824143410p:plain

選出

「画面上で選出したいポケモンにチェックを入れる」->「確定」の操作をすると、あとはそのとおりにSwitchにコマンドを送ってくれる。初めはSwitchの各種ボタンを一つ一つ押す操作をすれば問題ないだろうと考えていたが、操作に時間がかかり過ぎて選出を時間内に完了できないケースが多発したため、このような形になった。動きはこのGIFアニメのようなイメージ(右側にあるのがスマホの録画)

f:id:rocky_manobi:20200824143042g:plain

右下のスマホ画面で「確定」を選んだ後にSwitch側で選出操作が行われているのがわかる。 このときアイコンの画像認識で相手のポケモンの一覧も画像から判定しており、対戦中の動画解析結果と合わせることで自分と相手それぞれの選出のログを残せるようになっている。

行動選択

こちらもSwitchの操作一つ一つを入力する方式ではなく、利用できる技や入れ替えたいポケモンを直接選択できるようにしている。ダイマックスもできる。

f:id:rocky_manobi:20200824125612p:plain

その他

非戦闘中における操作は特別なUIを設けず、Switchのボタンを一つ一つWeb上に用意することにした。行動選択や選出のUIで不備がでた場合のバックアップとしても使っている(それから「降参」のための操作も実装していないので、降参したい場合はこれを使う)。以下の画像で一瞬でイメージは湧くと思う。

対戦ログ

自分の選出、相手の選出、使った技、誰にダイマックスを切ったかなどをログに残しくれて、あとから振り返ることができる。 正直これは開発をしていたら面白くなってしまって、あれもできるならこれも...とやっていった中で出来上がったものなので、解決したい課題に対して特にクリティカルには効いていない。(PC向けのバトルアシストツールを作っているのでそちらには応用する気満々)

## 自分
- ドラパルト[先発]
  - ドラゴンアロー,ゴーストダイブ
- ドリュウズ[dymax]
  - ダイスチル
- バンギラス
  - ステルスロック,でんじは,がんせきふうじ,
## 相手
- ドリュウズ[先発]
  - じしん,
- カバルドン
  - ステルスロック,ほえる,あくび,
- ラプラス[dymax]
  - キョダイセンリツ,ダイストリーム,なみのり,

ダメージログ

2ターン目
-[自分の] ドラパルトがカバルドンにドラゴンアロー - 57% のダメージ
3ターン目
-[相手の] カバルドンがバンギラスにステルスロック -
4ターン目
-[自分の] バンギラスがカバルドンにステルスロック -
-[相手の] カバルドンがバンギラスにほえる -
5ターン目
-[自分の] ドラパルトがカバルドンにゴーストダイブ -
-[相手の] カバルドンがドラパルトにあくび -
6ターン目
-[自分の] ドラパルトがカバルドンにゴーストダイブ - 8%(以上) のダメージ
7ターン目
-[自分の] ドラパルトがラプラスにゴーストダイブ -
-[相手の] ラプラスがドラパルトにキョダイセンリツ -
8ターン目
-[自分の] ドラパルトがラプラスにゴーストダイブ - 35% のダメージ
-[相手の] ラプラスがドラパルトにキョダイセンリツ - 88%(以上) のダメージ
9ターン目
-[自分の] バンギラスがラプラスにでんじは -
-[相手の] ラプラスがバンギラスにダイストリーム - 59% のダメージ
10ターン目(9)
-[自分の] バンギラスがラプラスにがんせきふうじ - 26% のダメージ
-[相手の] ラプラスがパ のンギラスになみのり - 24%(以上) のダメージ

ながら対戦アシスト機能

操作可能時サウンドアラート

操作する必要があるシチュエーションになったら「ピコ!」と音を鳴らしてくれる。具体的には、対戦相手がみつかって選出画面に遷移したとき、新しいターンに入って行動を選択できるようになったとき、ポケモンが倒されてポケモンを変えなければならないとき、の3つのシチュエーションで音がなる。ながらでやっているので必須。そもそも普通に公式にも実装して欲しい機能でもある。

ちなみに ON/OFFが可能。

バトルメッセージログ表示

公式に実装して欲しい機能その2。戦闘中に表示されたメッセージのログを見ることができる。 ながらで対戦をしているので、画面をずっと見ているわけにはいかない。見ていない間に何が起こったのかが分からないと行動を選択することができないので必須(そもそも普通に対戦していても相手の特性を見逃していて焦ったりする)

テキスト読み上げ

バトルメッセージをText2Speechで音声読み上げしてくれる機能。ON/OFFが可能(流石に)

いかにバトルメッセージのログを見られるとはいえ、行動決定に許された時間はわずか45秒。できればターンが回ってきたそのときには現状を把握している状態が望ましい。ながら対戦では画面をずっと見ておくことはできないので、音声読み上げでこれを補う。子供の寝かしつけのとき、公園で遊んでいるときなどなど、非常に重宝した。

作り方 / 開発記録

ここからは全体の構造や利用している技術、開発時に課題となり工夫を要した箇所などについて触れていく。

PCでSwitchを操作できるようにする

多くのサイトで紹介されているように Arduino LEONARDでSwitchを操作できるようにするとともに、FT232RL USBシリアル変換モジュールを組み合わせて、そのArduinoに対してPCからUSBシリアル通信で命令を送れるようにしている。

image.png

Switchの映像をPCに取り込む

普通にキャプチャボードを使ってHDMI経由で映像をPCに取り込む。 ちなみにキャプチャボードはこれを買った。

今回は下心としてWebで画像解析の真似事などをしてみたいという欲求もあったので、Webで実装した。以下のように、Webカメラの映像を取り込む時となんら変わりない方法で映像を取得できる。

f:id:rocky_manobi:20200824160523p:plain

f:id:rocky_manobi:20200824130730p:plain

全体構成

これらに、サーバサイド画像解析等々を加えるとこんな感じになる。

  • CLOUD
    • Appサーバ (Node.js / Socket.io)
  • Arduino LEONARDO + USBシリアルモジュール
    • シリアル通信で受け取った命令をSwitchにバイパスする
  • PC
    • Switch操作アプリ(Node.js)
      • Socket.ioメッセージ <=> USBシリアル通信変換app
    • 動画解析機(Webフロント)
    • 画像解析 / OCR等踏み台 サーバ(Node.js)
  • スマホ
    • スマホコントローラ(Webフロント)

各プレイヤーのやり取りは、動画解析機と画像解析サーバ間を除いて全てSocket.ioで行なっている。 書く前から感じていたが、これは図にしないとだめ。

戦況解析

先に紹介したUIを実現するには、どの画面が表示されているか、技やポケモンの選択肢、ダイマックス可否などを映像から判定する必要がある。これらの大半はフロントエンドで判定をしている。OpenCV.js等を使うと重さに耐えられなかった(バトルメッセージの表示速度を鑑みると解析頻度は10FPSは欲しい)のと、興味もあったので生のCanvas、特にUint8ClampedArrayなどのナマモノををかなり触った。

開発方法 - 機械学習の中の人を人力でやる

最初に一気にテストコードを書いて、期待の判定になるようにルールを書いていった。 テストデータは何戦か録画をして、そこから判定したいシチュエーションの画像を抽出して得た。

こんな画像を用意して

f:id:rocky_manobi:20200824132742p:plain

こんなテストコードを書いて

describe("getHp", () => {
  it("画面から相手と自分のポケモンのHPを読み取る(%)", () => {
    testCases.forEach((testCase) => {
      const ctx = getDrawedCanvas(testCase.file);
      const result = getHp(ctx);
      expect(result.myHp).toEqual(30)
      expect(result.opposingHp).toEqual(74)
    });
  });
});

あとはひたすらにロジックの中身を書き続ける。 ちなみにHPの判定はHPバーにしめる緑色/黄色/赤色ではない色の割合でパーセンテージを出している。

辛いところ

いくら直接HDMIから取り込んでいるからといっても、リアルタイムにエンコードしている関係上、画像の数値は同じ画面でも、同じメッセージ表示でも完全にピクセルの数値が一致することはない。黒い部分でも白い部分との境界近くではグレーに近づいたりなどもする。

画面の判定

(本当はもっとあるが)画面の種類としては主に以下のどれが表示されているかを判定する必要があった。これに関しても各画面で特徴的な部分のルールを書いて対処している(例えばバトルメッセージ表示中の判定は、適当に2値下して、下の方は黒だが、全体は黒ではなく、メッセージが表示される領域がxx%以上黒ドットではない、のような泥臭いロジックを書いている)。これもあらかじめ全ケースをテストコードを書いておくことで、精度を上げるために他の画面と誤検知した〜などの事故を起こさずに実装することができた。(そろそろ限界だけど)

  • 戦闘待機画面
  • 選出画面(相手側表示/非相手側表示)
  • 行動選択画面
  • バトルメッセージ表示中か否か
  • ポケモンのステータスやHPの変化を取得してもよい画面か否か

OCR

ポケモンの名前、技の名前など、文字の検出にはGoogle Cloud VisionOCRを使っている。 お金がかかるので、少しでもリクエスト回数を減らすためにローカルの画像処理で可能な限り重複リクエストを避けるようにしたり、複数の言語が混じっている場合(相手のポケモンが海外産だと名前も英語になる)でも精度を下げないような前処理を入れたりするなどの工夫をしている。今はおおよそ1マッチ100リクエスト程度(多分10円以下)。この辺りについては機会があればまとめておきたい。

複数の言語が混じっている場合への対処だけに触れると

f:id:rocky_manobi:20200824134116p:plain

こういうのを...

f:id:rocky_manobi:20200824133756p:plain

このように、スペースが空いているところを改行に見えるように画像を加工してからAPIを叩いてあげると飛躍的に精度が向上したりする。 こうすることで、戦況ログを保存するにあたっては十分な制度が得られた(逆にうまくいかなければおそらく作れなかった)

その他機能

音/テキスト読み上げ

音声の再生はWeb Audio API、テキスト読み上げはChromeのWeb Speech APIを使っている。 特筆することは特にないが、iOSではテキスト読み上げ機能はChromeのみで使用できる。ゆえにHome画面にショートカットを置いて起動した場合などには使えない。

本編終了

おしまい

おわりに

以上でスマホでSwitchを操作してポケモンランクマッチに潜るための装置の紹介を一旦終わります。読んでいただいた方、ありがとうございました。 予告した通り、開発記録+申し訳程度の開発Tipsだったとは思いますが、割と興味本位で無茶に無駄に工数をかけてしまっていることだけは分かっていただけると思います。

ちなみにこいつ自体は結構重宝して動いてくれてはしましたが、まだポケモンの方に結果がでていないので効果のほどはなんとも言えません。 (明確な開発成果といえば、過去2度試みて失敗しているVim->VSCodeの移行に成功したということくらい)

とりあえずこの先、ゲームをやるべきかツールをつくるべきかの天秤で悩んだりしつつあれもやりたいこれもやりたい試したいととっちらかってきているので、もし使ってみたかったり、一緒に作ってみたいという方がいらっしゃればTwitter DMででも連絡をいただければ幸いです。

おまけ - これから

新しいポケモンもさらに増えて困ったことになりそうな11月に向けて、次は直接的に戦闘をアシストしてくれるようなツールを作ってみようと思っています(作っている)。 戦況を自動で判別しつつ、相手の育成パターンによってこちらの技がどの程度入るのかをざっくりみられたり、逆にこちらが打った技がn%はいったのでおそらく耐久に振っている、、など、経験が浅くてダメージ感覚のない人でも初見ポケモンに殺されないようなものができると嬉しいです。

戦況解析からのダメージ計算の実現はとても複雑なドメインで、モデリングの勉強にとてもなることがわかりました。そのあたりを考えるのが楽しくてここ最近まったく進捗がでていないのですが、完成したらモデリング文脈でもなにかアウトプットできるといいなと思っています。

image.png

(あとすばやさ関係もいい感じのUIで表現したい)

それでは、次の自由研究に乞うご期待です。

f:id:rocky_manobi:20200824155000p:plain:w30

JavaScriptでちょっと複雑なcliを作るのに便利なEnquirer

LAPRAS アウトプットリレー の...何日目だっけ?3/25の記事です! こんにちは!LAPRAS エンジニアの @rockymanobi です!

最近Node.jsでCLIを作る機会があり、その時に触ったEnquirerというライブラリが便利だったので、軽く紹介してみようというものです。ツールそのものについて軽くふれつつ、制作過程で出てきた「こんなことしたいけど、どう実現すれば良いんだろう」と試行錯誤して分かった使い方などを共有できればなと思います。

Enquirerとは

Enquirerは CLIアプリケーションにおける対話的インターフェイスの実装を楽にしてくれるライブラリです。単純なテキスト入力の受付はもちろん、リストからの選択、チェックボックス、パスワード、入力補完、など、様々な入力方式を手軽に組み込むことができます。Node.js製です。JavaScript(TypeScript)万歳!

公式サイトによるとこんなのもできるそうです(凄い!! いつ使うんだろう

類似のツールとしては先発の Inquirer.js などがあります。公式サイトに「Inquirerより速いぜ!」とあったり、Inquirerとほぼ同じような記述方式をサポートしていることから、かなり意識して作っているように見えます。どちらも利用者が多く、メンテも続いているのでどちらを使っても良いでしょう。あえて比較をするならば、Enquirerの方が多少全体や中身を把握しやすかったのと、少し凝ったことをやろうとしたときに素直そうな印象でした(個人の感想です)。

基本的な使い方

例えばこんなものを作りたい場合は...

f:id:rocky_manobi:20200326022503g:plain:w400

const Enquirer = require('enquirer');

(async ()=> {
  const question = {
    type: 'select',
    name: 'favorite',
    message: '好きな乗り物は?',
    choices: ['パトカー', '救急車', '消防車'],
  };
  const answer = await Enquirer.prompt(question);
  console.log(`僕も${answer.favorite}が好きだよ`);
})();

このようにpromptメソッドにオプションを渡してやると、それに応じた方式で入力を受け付ける描画をしたのち、ユーザの入力が完了したときにPromiseresolveしてくれます。結果はオプションnameに指定したキーにぶら下がってきます。

この要領で公式ドキュメントを見ながら使えば、大抵のことは実現できると思います(少し違う使い方もありますが後で触れます)

少し複雑なケースの実装

ここからは少しだけ複雑な要件に対応してみます。 複雑な入力と言われて最初に思い浮かぶのはポケモンバトルの選出画面です。故にここからは「ポケモンバトルの選出画面をCLIで実装するとしたら」をテーマに少しづつ進めていきたいと思います。

リストから要素を複数選択(これは単純

ポケモンの対戦は、基本的に以下のような流れで進行します。

1. ポケモン6匹でパーティを構築する
2. 対戦前にお互いにパーティを見せ合う
3. 6匹のうち3匹を選出し、3vs3で対戦

今回の対応範囲である「選出」というのはこの3番めにある「6匹のうち3匹を選ぶ」作業のことを指します。

以上を踏まえると、選出画面のCLI版は以下のようなものになりそうです。

f:id:rocky_manobi:20200326022638g:plain:w400

実装は以下のようになります。

const Enquirer = require('enquirer');

const myPokemonNames = [
  'フシギバナ',
  'リザードン',
  'カメックス',
  'ゴリランダー',
  'エースバーン',
  'インテレオン',
];

(async ()=> {
  const question = {
    name: 'selections',
    type: 'select',
    multiple: true,
    message: '誰を出す?',
    choices: myPokemonNames,
    validate: (selectedItems) => {
      if(selectedItems.length === 3){
        // true/falseを返すとOK/NGのみを表現
        return true;
      }
      // 文字列を返すとエラーメッセージになる
      return '3匹選んでください';
    },
  };
  const answer = await Enquirer.prompt(question);
  console.log(`${answer.selections.join(',')} を選出しました`);
})();

オプションmultiple: trueを渡すことで、複数選択可能なチェックボックス式の入力を受け付けます。

また、validateにバリデーション用の関数を渡すことで、ユーザ入力を検証して、不適合な入力を弾き、入力画面をキープすることができます。エラーメッセージを独自のものにしたい場合は、true/falseではなく文字列を返すようにすることで、判定をNGとしたうえで、関数が返した文字列をエラーメッセージとして表示してくれます(errorMessageってオプションあったほうがわかりやすいきがしますが)。

タイマーで入力をキャンセルする

ここまでは公式Readmeにもしっかり書いてあるので難なく対応できました。が、要件を一つ忘れていたのでここで追加します。

ポケモンの対戦では遅延行為を防ぐため、あらゆる行動に制限時間が設けられています。もちろん選出も例外ではなく、すべてのプレイヤーは1分30秒(記事執筆時点)以内で3匹のポケモンを選び出す必要があります。これに間に合わない場合は、強制的に上から順に3匹のポケモンが選出されます。

この要件をCLIに反映させてみます。

f:id:rocky_manobi:20200326022748g:plain:w400

const Enquirer = require('enquirer');

// (信じられないことに)配列 myPokemonNamesをchoicesオプションとして渡すと破壊的に配列が変更されるので、
// 違うArrayインスタンスを返すようにFunctionに包んでいる
const myPokemonNames = () =>{
  return [
    'フシギバナ',
    'リザードン',
    'カメックス',
    'ゴリランダー',
    'エースバーン',
    'インテレオン',
  ];
};

(async ()=> {
  const prompt = new Enquirer.MultiSelect({
    name: 'selections',
    message: '誰を出す?',
    choices: myPokemonNames(),
    validate: (selectedItems) => {
      if(selectedItems.length === 3){
        return true;
      }
      return '3匹選んでください';
    },
  })

  let timer;
  prompt.once('run', ()=>{
    timer = setTimeout(()=>{
      prompt.cancel()
    }, 5000)
  })
  prompt.once('close', ()=>{ clearTimeout(timer); });

  const answer = await prompt.run().catch(() => {
    // 時間切れです
    return myPokemonNames().slice(0,3);
  });

  console.log(`${answer.join(',')} を選出しました`);

})();

これまでとは少し実装方法を変えています。これまではInquirer.js風の実装をしていましたが、ここではコチラのように、ライブラリにビルトインで入っているPromptの子クラスを用いた実装にしています。

コンストラクタの形式はこれまでpromptメソッドに渡していたものに似ていますが、各クラスごとに自明なものMultipleSelectクラスにおけるtypemultiple:trueなど)が不要になっています(TypeScriptなら型チェックも効いて快適)。

この方式ではPromptクラスのrun()メソッドを実行したときにユーザの入力を受け付けるようになり、同cansel()メソッドを呼んでやることで、強制的に入力を終了させることが可能です。

PromptクラスはEventEmitterを継承しており、上記コードでは入力受付開始時のrunイベントに反応してタイマーを作動させ、一定時間経過後にキャンセルするようにしています。入力終了時(キャンセル/タイムアウト含む)に発生するcloseイベントが発生したタイミングでは、タイマーを止める処理を実行するようにしています。

最後に、プロンプトをキャンセルしたときにはPromiserejectされるので、catch節でエラーを拾って、「時間切れの場合は先頭の3匹強制選出」を示す結果を返すようにしています(ちなみにcatch節のコールバックには何も引数が入って来ません)

カウントダウンを表示する

制限時間を超過すると時間切れになるようにはできましたが、選出中に「後何秒?」が分からないのは辛いものがあります。これも対応しましょう。

f:id:rocky_manobi:20200326022819g:plain:w400

(async ()=> {
  let timeRemaining = 10;
  const prompt = new Enquirer.MultiSelect({
    name: 'selections',
    message: () => { return `誰を出す? 残り ${timeRemaining} 秒` },
    choices: myPokemonNames(),
    validate: (selectedItems) => {
      if(selectedItems.length === 3){
        return true;
      }
      return '3匹選んでください';
    },
  })

  let interval;
  prompt.once('run', ()=>{
    interval = setInterval(()=>{
      timeRemaining -= 1;
      if(timeRemaining <= 0){
        prompt.cancel()
      } else {
        prompt.render()
      }
    }, 1000)
  })
  prompt.once('close', ()=>{ clearInterval(interval); });

  const answer = await prompt.run().catch(()=>{
    console.log('時間切れです');
    return myPokemonNames().slice(0,3);
  });
  console.log(`${answer.join(',')} を選出しました`);
})();

タイマー処理をsetTimeoutsetIntervalに変えて、1秒毎にカウントダウンするように変更しつつ、メッセージに「残り n 秒」を表示させるために、以下の修正を施しています。

  • messageオプションに残り秒数を表示する文字列を返す関数を渡す
  • 1秒毎に prompt.render()メソッドを実行する

これにより、プロンプトの内容が毎秒再描画され、残り時間がカウントダウンされていく様子を表示することができました。カウントダウン部分のみを他のライブラリや独自実装などで代替しようとすると、カーソルの状態が衝突して表示がおかしくなったりするので、このあたりをサポートしてくれているのは有り難い限りです。

複数の質問をする & 前の回答を考慮して選択肢を変更する

これで完成かと思いましたが、そうはいきません。確かに選出は6匹から3匹を選び出す作業ですが、同時に「誰を先発させるか」を決める作業でもあることを忘れていました。

最初に選択する要素には特別な意味をもたせる必要がありそうなので、最初に先発を聞いて選んでもらった後に、控えの二匹を選出してもらうようにしてみます。

f:id:rocky_manobi:20200326022904g:plain:w400

(async ()=> {
  let timeRemaining = 10;
  let currentPrompt;
  let interval;

  // Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく
  // new Enquirer()して、そいつのpromptメソッドを呼ぶようにする
  const enquirer = new Enquirer();
  enquirer.on('prompt', (prompt) => {
    currentPrompt = prompt;
    prompt.once('run', ()=>{
      interval = setInterval(()=>{
        timeRemaining -= 1;
        if(timeRemaining <= 0){
          currentPrompt.cancel()
        } else {
          currentPrompt.render()
        }
      }, 1000)
    });
    prompt.once('close', ()=>{ clearInterval(interval); });
  })

  const answer = await enquirer.prompt([
    {
      type: 'select',
      name: 'starter',
      message: () => { return `先発は誰にする? 残り ${timeRemaining} 秒` },
      choices: myPokemonNames(),
    },
    {
      type: 'select',
      multiple: true,
      name: 'reserves',
      message: () => { return `控えは誰にする? 残り ${timeRemaining} 秒` },
      choices() {
        return myPokemonNames().filter((name) => {
          return this.state.answers.starter !== name;
        })
      },
      validate: (selectedItems) => {
        if(selectedItems.length === 2){
          return true;
        }
        return '2匹選んでください';
      },
    }
  ]).catch(console.error);

  if (answer) {
    const selected = [answer.starter].concat(answer.reserves);
    console.log(`${selected.join(',')} を選出しました`);
  } else {
    console.log('時間切れです')
    console.log(`${myPokemonNames().slice(0,3).join(',')} を選出しました`);
  }
})();

公式ドキュメント によると、Enquirerは複数の質問を連続して表示することに対応しているようですが、Enquirer.promptメソッドにオプションの配列を渡してあげる形式にする必要があります。このままだとタイマー処理によってキャンセルすることができないので、どうにかしてPromptクラスのインスタンスを参照する必要があります。

ということをやろうとしているのが上の方にあるこの処理です。

  // Enquirerインスタンスの参照が欲しいのでstaticメソッドのpromptではなく
  // new Enquirer()して、そいつのpromptメソッドを呼ぶようにする
  const enquirer = new Enquirer();
  enquirer.on('prompt', (prompt) => {

Enquirerクラスは内部的に保持しているPromptインスタンスを処理するタイミングでpromptイベントを発火しつつPromptインスタンスを渡してくれるので、そこでこれまでのケースと同じようにイベントハンドルを仕掛けています。

先発で選んだポケモンを控えの選択肢に出さない

messageオプションなどと同様にchoicesオプションにも関数を指定することが可能です。そして、この関数内部でthis.state.answersを参照することで前の質問に対する入力の値を得ることができます。コレを利用して、控えポケモンの選択肢から、先発に選んだポケモンを除外しています。

      choices() {
        return myPokemonNames().filter((name) => {
          return this.state.answers.starter !== name;
        })
      },

その他

今回の例ではchoicesにはString配列を渡していましたが、{ name: '興梠', value: 'rocky' }のようなオブジェクトの配列を渡すことで、見た目上はnameに指定した値を表示しつつ、実際にanswerで得られるのはvalueに指定した値にする、ということも可能です(多分大体そうする)。

加えて、このようにオブジェクトを渡す方式にしている場合は、最後の例を実現するにあたってchoicesをフィルタする代わりに、各choiceのオブジェクトにdisabled: trueなどを渡すことで選択不能な状態にすることができるみたいです。

(というか複数聞きたいなら複数回prompt呼んでしまえばいいじゃないのって思ったけど違うのかな)

まとめ

無事、ポケモン選出画面の要件を満たすことができました。 技術的には以下のあたりがリポジトリ検索したり調べたりコード見てみたりしないと見えてこなかった印象があるので、実現したい方は参考にしてみると良いでしょう。(PullRequestチャンスでもある)

  • タイマーでプロンプトを終了させるためにはPrompt#cancel
  • Promptクラスの参照は、最初からPromptクラスを直接使った方法で実装するか、Enquirerクラスのpromptイベントをリスニングして降ってくるのを拾う

最後に

この謎チュートリアルはノンフィクションです(実際に勢いでポケモン対戦できるCLIを書いているときの展開をほぼそのまま再現しました)

新しいチームに加わるエンジニアのための "被" オンボーディングガイド v0.1.0

この記事はLAPRAS Advent Calendar 2019の13日目の記事です

自分も最近体験したということもあり、LAPRASのWebAppチームのオンボーディングについて紹介しようと思ったが、最近自社推しが過ぎる気もしたので、視点をオンボーディングを受ける側に移し「こんな風に立ち回ろう!」というスタンスで書くことにしました。

v0.1.0 ということで走り書きの勢いでそのままお届け致します。

オンボーディングとは

オンボーディングとは、新たにサービスを導入した人や新たに組織に参加した人などに対して早く慣れることができるようにサポートすることです。

この記事の文脈は後者。 注意することやTIPSを書いていきたい。自分にも多少刺さりまくることがあるが、過去はもっと酷かったところが若干改善したと思っているので、勇気を持って書くことにする。

いきなり目立った成果を出そうとして消耗しない

まずは危険性を認識する

  • 新しいチームに入ってまず最初に戦わないといけないのは「自分、役に立ってるの?必要なの?」「自分は何者なの?」という不安感
  • どのような人でも、環境が変わればまずは学びのフェーズから始まる(エンジニアで、ある程度システムが大きくなっている場合は特に)
  • チームに入りたての学びのフェーズでは貢献は愚か、自己認識としてはマイナスの存在とも感じてしまう
  • 以前の職場でのパフォーマンスが高かったり、重要なポジションを担っていたりするほどギャップが激しく、精神的負荷が高い
  • 早くその状態を脱出しようとして焦り、目に付くインパクトの大きそうな課題やできそうなことを片っ端からやろうとするが、成果が出ずに自己肯定感が下がって消耗する
  • この過程に耐えられず、特に(想像上の)他者の認識から身を守ろうとして、他責的な振る舞いになって信頼を失う。これは危ない。
  • 地に足をつけて着実に積み上げていく方が長期的にチームにも貢献できるはず

以下、この危険性に対処するための方法を紹介する

目標とスケジュールを設定して共有する

  • 理想と現状のギャップに対して焦らないようにするためには、目標とスケジュールを決めるのが効果的
  • スケジュールがあれば「まだゴールには遠いけど、オンスケだから今日休める!寝るぞー」と思うことができる
  • この認識をチームと共有することで、自分も今の所大丈夫だと思えるし、周囲にもそう思ってもらえていると安心できる

ちなみにLAPRASでは入社前から以下のような期待値を提示されていた(内容はエンジニア向け、とは少しズレるが参考までに。純えんじにあ職ならばより技術的な貢献ができるような内容が並ぶはずだ)

* 1ヶ月
  * スクラムの開発チームに馴染んで(技術・内部仕様理解以外の点で)普段の業務に障害がなくなること(=普通に仕事がお願いできる状態)
* 3ヶ月 : 
  * 開発チームをまとめ、リードしていくことができる
* 6ヶ月
  * 予測不能(全社的に変化が激しいので、いまは明確にはできない
  • 入社直後に上記目標について改めて合意をし、合わせて必要なサポート(後述)についても認識を合わせる機会があった
  • 一開発者としてだけではなく、チーム全体への貢献を求められるポジション枠で入社していたということもあり、「まずは普通の開発者として一人分の仕事ができるようになることにフォーカス」と思えたことは学習フェーズの不安との戦いに大きく貢献した。(仮に僕が開発サイドオンリーで戦える強者であるならばとにかく開発集中でよいと思うが、残念ながら自分の技術レベルはそのレベルには達していない)
  • このような機会が無い場合はリクエストしよう

進捗を共有して必要であればスケジュールを変更する

達成できない目標は精神衛生上悪影響があり過ぎるし、達成できたと思っているのが自分だけだというのもまた悲しい

フィードバックをもらって現状を正しく認識する

自己認識でうまくいっていると思っていても、他者からはそう見えていないこともある。チームに貢献するという文脈において、このギャップは解消すべきだ。逆に、自分ではダメだと思っていても、他者からはよく見えているところもある。これは本来なら無用な悩みになるので、このギャップも解消すべきだ「●●の目標に対して、自分としてはこの点とこの点はできていると思っている。ここは足りないのでこうしようと思っている。認識はあっていますか?」という質問ができると良い。

ギャップがあれば解消する方法を考える
  • 世の中うまくいかないこともある
  • どうすれば修正できるのか、どのようなサポートが必要なのかを改めて対策を考えたり、助言を求めたりする
  • 目標やスケジュールの達成見込みが絶望的ならば、期待値かスケジュールを調整する

LAPRASでは主に毎週(慣れてきたら隔週)の1on1と、月一の振り返りで上記のような作業を行なった。同じく、このような機会がなければリクエストしよう。

(おまけ)結果

今は入社4ヶ月になるが、概ねギリギリ大丈夫と言えるレベルだと思っている。次のステップに向けて新しい課題に四苦八苦しながら対処しているところでもある。

受ける側の話とはずれるが、先輩社員に「焦って結果出そうとして苦しんで潰れていく人が多いから焦らないで一つずつやっていった方がよいですよ」と言葉をかけてもらったことをすごく覚えている。いくら自分がそのつもりで、目標を共有していたとしても、焦る気持ちはゼロにはならないので、言葉にしてもらえるて死ぬほど楽になったのを覚えている。

現状の仕組みを作り上げたことに対する敬意と感謝を忘れない

改めて言語化しようと思ったらTweetがあった。連投しているので気になる方にはみてもらえると嬉しい。大事なのは「味方感」。本当の意味で味方になることだ。

  • 課題があるのは当たり前、むしろ課題が無いなら自分がいる意味がない。自分は被害者ではない。
  • そもそも自分で今の状態を一から作れるかかんがえる(多分NOだから起業せずに入社したんだろう)
  • まずはそのすごさを認めて敬意を表し、チーム全体を自分にとって善の存在として認識する
  • その意識を忘れなければ細かい表現は気にしなくていいはず
  • 味方として一緒に解決に取り組めば良い

ちなみに受け入れ側として注意したいのは以下のような力学(そしてLAPRASのチームは信じられないくらいこの手のネガな部分が無くて好きだ)

実務上のサポート

エンジニア向けと書いたので、実務についての工夫も書いておく。 先ほど書いた「必要なサポート」の具体的な内容はおもにこちらにリンクする。ガンガンリクエストしていこう。

システムを触る

  • 自分たちが何を作っているのか理解せずに物を作るのはまずい。機会がなければ確実にリクエストしよう。

システム全体像のインプット

  • 最初に全体像を説明をしないところはないと思うが、されなかったらリクエストしよう
  • DB定義、依存ライブラリ/サービス、全体の設計思想、インフラの大まかな構成 などを中心に聞く
  • 一度に理解する必要はない。実務をやりながら理解を深めていくイメージで良い。

ペアプロ

  • コード理解において「(目的もなく)コード読んでおいて」はワークしないので、「とりあえずコード読んでおきます!」という宣言はしない
  • 任せるタスクが無い、余裕が無いのであればとりあえずペアプロをした方が効率が良い
  • 触っている箇所だけではなく、実装の背景など、気になったところは質問してドキュメントに落としきれていない知識を吸収することに務める
  • LAPRASでは最初の2週間の開発タスクは全てペアプロで実施、3週目からは自己申告で「これはソロ狩りしたい」といったものだけソロで対処するスタイルをとった
  • 1ヶ月で開発タスクをこなせるようになる、をクリアすることに大きく貢献した

  • もし現場でペアプロがあまり推奨されていない場合は、せめて対面でのコードレビューを求めよう(コードをみながらの会話は必ず必要だ)

ペアOpsもといペアワーク

  • 現場で実務をこなす際に必要なのはコードを書くことだけでは無い
  • 障害対応や、システム面の質問に答えるための調査をしたり、データ周りの調査などもそれに含まれる
  • これらの作業はやってみないと振られないので、自分からリクエストしにいく
  • とはいえいきなりは任せられないので「自分のやったことのない仕事やるときは声かけてください隣でみさせてください」と宣言する
  • shellの設定や使っている便利ツール、システム運用時に利用しているサービス、などなど、入社後の説明では得られなかった情報を発見することがある
  • 周囲のエンジニアのレベル感もわかる(この点自分は大いに焦りを覚えたのだけど)

自分の関わっていないPullRequestを見る

  • 最初はどうしても自分のタスクだけを追いがちになるが、最低限チームが何をやっているのかは把握したいところ
  • Issueは「これからすること」が書いてあるが、PRはその結果までついているので、初期はPRから見る方が効率が良い
  • 文字通り直近書かれたコードを見ることができるので「昔はこういう書き方をしていたけど今は...」のような事故が防げる
  • レビュー指摘の傾向などからチームの「大事にしているところ」や「雰囲気」も感じ取ることができるはず
  • (スクラムをやっているならば、計画MTGの設計で話していた内容が、どうコードに落としこまれるのかを意識して読むと良い)
  • みてはダメだと言われることはないが、時間をとれないようであればリクエストしてみよう

スプリント計画をちゃんとやるスクラムチームはEasyモード

  • 本筋とは少しずれる
  • もし新たに入るチームがスクラムを採用している場合、事前にスプリント計画の様子を聞くと良い
  • スプリント計画ミーティングにおいて、作るものと設計について詳細に議論して合意しているチームは、当然ながら新しく入った人のキャッチアップも早い

まとめ

  • ざっとみると、チームに入る側としては周囲に労力をかけてもらうようなリクエストが多いように見える
  • とはいえ新しくきた人をワークさせるのに労力がかかるのは当然なのでためらってはいけない
  • ためらって、結果としてパフォーマンスが下がればその方が損失が大きい
  • 多少ずうずうしく、学ぶために周りを巻き込んでいく姿勢が大事
  • 組織としては、その辺に気を配れるかが大事

その他

  • こまかいメンタル的なところなどはキャラ的に敏感ではないので書かなかった

最後に

偉そうに書いたが完全に自分の出来栄えは棚に上げた理想論だと思って読んでほしい。

多少の貢献はできているかもしれないが、まだまだチームの力になるにはどうすれば良いか割と必死だ。気を抜いたらただのお荷物やで自分!という感覚とは普通に戦っている。チームの皆様にはその辺は生暖かく見守りつつ、サポートも頂いているのでそれに救われている。徐々にやれる範囲を広げてきたのもあって抱えているものが増えてきたので、次は仕組みにして手放していき、パフォーマンスをスケールさせたい。

このように全然余裕な雰囲気はない。やっていきしかない。

というわけで、v0.1.0よろしくプロットのような描きっぷりになってしまったが、少しは有益だと思うのでその辺は容赦してほしい。(万が一)リライトしたらTwitterに書くので、とりあえずその時までみんな頑張ろう。

SlackでDMを受け取るとパブリックチャネルに優しく誘導してくれるDM警察というBOTを作って公開しました

この記事はLAPRAS Advent Calendar 2019の9日目の記事です

f:id:rocky_manobi:20191210010511p:plain

概要

  • LAPRASに入る前、業務の内容をDMでたくさん受け取って辛いという課題があった
  • DMを受け取るとパブリックチャネルに優しく誘導してくれるDM警察というBOTを作ったがLAPRASに入ったら需要がなかった
  • なんでや?じゃあLAPRAS関係ないやろ!?
  • まとめ

課題 - 業務の内容をDMでたくさん受け取るのは辛い

このような内容をDMで受け取ると少し困ります。

f:id:rocky_manobi:20191210010546p:plain

パブリックチャネルの発言であれば、僕がこの質問に答えられない場合でも「@知っていそうな人 さん、わかりますか?」とメンションするだけで事が足ります。たまたま会話を見ていて知っている人が能動的に答えてくれる事もあるでしょう。また、後々一連の会話をシェアしたくなったときでも、この会話へのリンクを貼れば経緯を伝えることができます。DMではいずれも叶いません。

本当に秘匿したい情報のやりとりを除いた殆ど全ての場合において、情報はパブリックでやりとりする方が良いはずです。良いはずですが、この手のやりとりをDMで行う人の割合は決して少なくありません。

以前の僕も、そういったDMを受け取るたびに上記の説明をしつつ、なるべくパブリックチャネルにポストしてもらうよう促していました。そして、これはおそらく日本のあちこちで行われている事でしょう。

DMを送る方には悪意がない

一方でDMを送る方としては、単純にパブリックチャネルに発言するメリットを理解していなかったり、使い方を知らなかったり、オープンな場所に投稿することをためらってしまうような空気があったりと、必ずしも悪意があるわけではありません。そこが悩ましいところです。

丁寧に啓蒙していくしかないのですが、やはり続くと辛い。そして言い続けているうちに、段々と「うるさいやつ認定」されていくのもまたつらい。

DM禁止は筋悪(だと思っている

一律禁止はしたくない、というスタンスです。有用なシチュエーションでは普通に使うべきだと思います。

なにより、大事なのはDMをしない事ではなく、パブリックに情報をやりとりすることです。その大切さを伝える方向で解決したいところです。

DM警察を作って公開しました

というわけで、諸問題を解決するBOTを作りました。ざっと特徴は以下の通りです。

  • DMを受け取ると、BOTがパブリックチャネルに誘導してくれる
    • コミュニケーションは職質風。パブリックチャネルに投稿する意義を啓蒙してくれる
    • ゴネれば10分黙ってくれる
  • 簡単に使える
    • Slackワークスペース公式サイト からインストールできる
    • Botにメンションする事で、DMの監視を依頼できる(監視してほしい人だけが使うことができる)
  • 権限が気になる人は、Herokuボタンで独自環境を構築することができる

DMを受け取ると、BOTがパブリックチャネルに誘導

DMを受け取ると、DM警察が現れてDM送信者に職質をかけてくれます。無視をしてもDMを受け取るたびに起動するので、あまりのウザさに流石に無視はされません。

f:id:rocky_manobi:20191210010511p:plain

拒否すると、パブリックチャネルに投稿する意義などを教えてくれます。「我々も仕事でやっているのですみません」という感じで、とてもウザい感じに仕上がっています。

f:id:rocky_manobi:20191210010836p:plain

ゴネ続けると、最後には諦めて許してくれます。10分離脱してくれるので、その間に会話を済ませることもできます。

f:id:rocky_manobi:20191210010417p:plain

Slackワークスペースに公式サイトからインストールできる

こちらが公式サイト です。動画やマニュアル、注意事項などが書いてあります。

「dm警察をslackにインストールする」ボタンを押すとSlack Appがインストールされます。インストール時にAppのWebhookを送るチャネルを聞いてくるので、あらかじめ「#DM警察」などを作っておくと良いでしょう。

また、誤ったワークスペースにインストールしないようにご注意ください。

Botにメンションする事で、DMの監視を依頼できる(監視してほしい人だけが使うことができる)

こんなふうに「パトロールよろしく」とメンションすると、パトロールしてくれます。

f:id:rocky_manobi:20191210010341p:plain

Herokuボタンで独自環境を構築することができる

DM警察のサーバはDMの内容を記録していませんが、DMを読めるという強めの権限を渡すこと自体がネックになるという方もいらっしゃると思い、ソースコードを公開してHerokuボタンも設置しました。コチラを参考に独自の環境を構築する事ができると思います。やってみてね。

あと、リポジトリにスターつけて頂けるとメンテ続ける意欲が強化されるので、気に入った方は是非ぜひ。

github.com

運用TIPS: 口うるさいのは人ではなくDM警察

DM警察がきたからには、もう口うるさく啓蒙する必要はありません。警察を呼んだのです。任せましょう。

たまにBOTを無視して会話する猛者も現れますが、そのときは「BOTがうるさいので、パブリックチャネルいきましょうよ」「DMめちゃ送ってくる人がいて(あなたなのだけど)、その人対策でDM警察いれたんです。すみません警察がうるさくて」などといって、DM警察という共通の敵を作りながら、パブリックチャネルに誘導してしまいましょう。

言いにくいことは機械に言わせる。社会性フィルタを通した言い回しも、一度考えればそれで終わりです。繰り返していいんです。DM警察は機械ですから。

と、このように運用をした結果、以前よりDMを受け取ることが減りました。

なんでや?LAPRAS関係ないやろ!?

そうおっしゃらずに、まずはこちらをご覧ください。

ご覧の通り、弊社LAPRAS株式会社のSlackは発言の殆どがパブリックチャネルにポストされており、全社的に非常にオープンな状態であるといえます。業務をしていてDMを受け取ることは稀で、その内容もパスワードなどの秘匿すべき情報や、休日の遊び関係の会話(子供どうしで遊ばせる会のために集合場所を決めたりetc)などであり、なんら違和感を覚えることはありません。

もうお気づきかと思いますが、このような環境ではDM警察に活躍の場はありません。オープンな組織では、DM警察は生きていけないのです。4ヶ月前にLAPRASに入社した僕はそのことに気づき、そして...DM警察のDynoを「0」に設定しました。当時のDM警察は僕だげが使えるレベルにしか整備がされていなかったため、その日がDM警察の命日となりました。

それから...

DM警察を止めてから数ヶ月が経ったある日、平和なSlack生活を送っていた僕はTwitterであるものを目にしました。それは「Slackで業務連絡がDMで送られてきて辛い」というツイートでした。多くの共感を集めたそのツイートは拡散され、議論の火種となり、一定の周期で僕のTLに「Slack DM辛いネタ」を届けるようになりました。

「僕は解放されたけど、世の中にはまだまだ苦しんでいる人がいるんだ。解決策を持っているのに、自分が救われたからといって電源を落として満足している、こんなことで良いのか?DM警察にも申し訳ないのでは?」TLをみるたびにそんな気持ちになりました。そしてこの「うしろめたさ」こそがDM警察を公開した動機です。

もしLAPRASに入らず、あのままDM警察が稼働していたら、きっと僕はある程度の満足感を持って過ごしていて、DM警察を公開することはなかったでしょう。DM警察を一度はリストラしたという後ろめたい気持ちが、制作の原動力になったのです。その意味で、DM警察を公開するにあたってLAPRASは大いに関係していると言えるでしょう。

まとめ

かくして、色々なところで運用しながら育ててきたDM警察でしたが、LAPRAS入社と同時に晴れてお役御免となりました。これからは僕のためにではなく、世の中の同じ悩みを持つ人のため、組織のために働き、LAPRASのようなオープンな文化の組織が増えていくことに貢献してほしいと願っています(若干自社を褒めすぎたかとも思いましたが、そこはまだ入社4ヶ月で、作り出したものよりも既存の枠組みに乗っている割合の方が多い今だからまだ許されることでしょう。とはいえそろそろ「これは俺がやったったで!ドヤァ」という成果を出したいものですね)。

それでは、快適なSlack Public Channel ライフを!

そして最後に

最後に 「それから...」の下りは全くの嘘です 。DM警察を公開したのは、MushupAwardsもといヒーローズリーグ2019に応募するネタを考えていたら「そういえばDM警察があったやん!あれでいこう!もったいないから公開しよう!」と思ったからです。嘘をついた上にLAPRAS本当に関係なく、誠に申し訳ありませんでした。

つまるところ「オープンな文化はいいですよね。LAPRASのそういうところが僕は好きです」というエントリだったと受け取って頂ければと思います。はい。申し訳ありませんでした。

それでは、快適なSlack Public Channel ライフを!

宣伝

反響いただいたのと、ちょくちょくご利用いただいているっぽいので、少しは役得を...ということで、宣伝を!させてください! LAPRAS株式会社は現在 WebAppエンジニアを絶賛募集中でございます。

DM警察は稼働していませんが、ご興味ありましたらこちらをご覧ください。

t.co

こういったものを作っています

lapras.com

こんな組織を目指しています

www.wantedly.com