Slay the Spire: 細かすぎて伝わらない攻略メモ

ちょっとかじったら読めるようになるもの。 カードの詳細は http://slay-the-spire.wikia.com/wiki/Slay_the_spire_Wiki にて。

アプデで頻繁に変更が入ってるけど現時点の話。

基本的な考え方

一回のプレーでなんでもやろうとしない。どうせ30分から2時間程度終わるので特化ビルドを狙って高速で死んだほうが学ぶものが多い。

優先順位は Energy > Power > Draw > 0 Cost カード > 状態異常 > ダメージ > Block

Block は意図して稼ぐものではなく、Draw カードの副次効果 や Power によるパッシブ効果で、ダメージ効率を稼ぐついでに得るもの、とした方がいい。そのメカニズムが完成するかどうかで Stage 3 のラスボスを倒せるかどうかが決まる。(狼ボスだけは弱いので倒せたりするが…)

大抵は自分が強すぎるかジリ貧でたどり着いて瞬殺されるかのどっちか。

デッキの圧縮

最初の Strike や Defence はかなり弱い部類なので出来るだけ早めに破棄して強いカードに入れ替えたい。他のカードを引きやすくなると思うと相対的に強くなる。ドミニオンで礼拝堂が強いのと一緒。

イベントやショップでは、自分は大抵、攻撃カード、防御カード、攻撃カード、防御カードと廃棄している。ランダムイベントでのトランスフォーム(実質ガチャ)の対象でもある。

Shopのマスでは1回につき1回だけ金を払ってカードを破棄できる。コストは 75 => 100 > 125 > 150 と上昇する。廃棄のチャンスは少ないので積極的に金が溜まったタイミングで訪問を狙いたい。

ただし、 Ironcrad で Corruption を引いた場合は 防御カードが Exhoust になるので廃棄しない。Vampireイベントで Strike を Bite(攻撃の度にHP+2回復) に入れ替えられるときは大抵やる。このゲームは最大HPよりHP回復手段の方が貴重。

Power や Exhoust カードは使うと山札からなくなるので実質圧縮になる。ドロー系のカードも圧縮になるが、Energyを余らせてるどうかで考える。

圧縮が上手く回ってるときは積極的に 0 コストカードを増やす。

篝火の Smith はカードを増やさず質を上げるので圧縮みたいなもの。ドロー系、序盤は Weak や Vulnerable などの状態異常の手段を優先する。

Ironcrad の Armaments+ は手札の全てのカードをアップグレードできるので実質圧縮みたいなもの。そして共通カードの Apotheosis があるならドロー系以外の強化は一切不要。初手はひたすらドローして Apotheosis をひねり出して使う。0コストでこれは正直強すぎる。ショップで見かけたら最優先で買う。たぶん220金ぐらい。

追記: 数日前のパッチで Apotheosis が 2(1) コストになっていたとのこと

狙いたい Boss Relic

ボス倒した時の入手Relicは比較的入手しやすい。 他のRelicの入手ランダムなので、何かのRelicに依存したビルドを最初から狙うのはおそらく機能しない。

Sozu

Potion が使えなくなる代わりにEnergy +1 / Turn 後半は Potion が相対的に弱ってるのでボスレリック取る頃なら自分を 1.33倍強化したほうが遥かに強い。

Cursed Key

2ボスで取得する場合、確定Relicマスが1個あって実質 Curseが +1枚で Energy +1 / Turn と考えると美味しい。終盤は Curse 一枚無視できるぐらいデッキが回ってるはず。

Pyramid

毎ターンのドローが -1 枚される代わりに、ターン終了時に手札を捨てなくて良くなる。 とくに2~3の高コストカードを任意に使える用になるのが大きい。

おすすめのカード: Ironcrad

Corrpution

すべてのスキルカードが Exhoust になりコスト 0 になる。高速な圧縮手段でもある。 ただし Skill 以外の Block 手段を確保しておかないとBlock手段を失って死ぬ。育ちきってない序盤に手に入れるとありがち。

ドロー+BlockのShrug It Off などが相性がいい。Barricadeと組み合わせるとたぶん負けることはない。どっちもレアだが。

Barricade

毎ターン Block がリセットされなくなる Power。単純に強い。事故らなくなる。 毎ターン Block -15 で維持というレリックで代替できることもあるが、そんなに手にはいらないので狙う必要はない。

これが手に入ってるときは 1コストで Block = ダメージの Body Slum を狙いたい。

Pommel Strike

コスト1 で ドロー1 + ダメージだが、強化すると ドロー2 になる。1コストでドロー2は強い。レアリティが低く序盤で手に入りやすいので序盤で手に入れて強化したい。

Battle Trance

そのターンそれ以上ドローできなくなる代わりに、0コストでドロー3(4) 他のドロー系を使い切ってから最後のひと押しに使いたい。

おすすめのカード: Silent

Neutralize

初期から持ってる 0 コストで ダメージ + Weak1(2) 1回強化すると Weak を切らさず回せるようになるので、他の Weak 手段を切れる。 ダメージもそこそこあるので、あと一発という痒いところに手が届いたりする。篝火で最優先で強化したい。

Ironcrad の強みが初期の戦闘ごとに HP回復6 の Relic なら、 Silent の強みは このカードの存在だと思う。

Noxious Fumes

毎ターン 2(3) の毒ダメージを敵全体に与える。 長期戦に強いのだが、Str強化を持ってる敵を前にするとダメージ出力が回る前に殴り負けるので注意が必要。

おそらく初期にアンロックされてるカードの中だと自然と主力になる。

Backflip

1コストでドロー2しつつ Block+5(8)。バランス良く強い。SilentはEnergy回復手段が豊富なので、何枚あってもいい。

Footwork

Dex +2(3) Dex+の手段は貴重。Backflip などと組み合わせてると良い。

Accuracy

Shiv のダメージを +3 の Power。 Cloak and Dagger や Infinite Blades と一緒に使いたい。QuickStepと組み合わせると手数で高速に Block を稼げる。Exhoust に反応する Passive とも相性がいい。

Unlock がやや遅いのが難点。

Bullet Time

それ以上ドローできなくなる代わりに 3(2)コストでそのターン Skill が使い放題。ドローしきってから使うか判断したい。

デッキ構築ローグライクダンジョン の Slay the Spire が面白かったので紹介したい

デッキ構築 + ローグライク + ダンジョン。

アーリーアクセスで少々コンテンツが足りない感はあるものの、とても面白かった。日本語の wiki や攻略情報が現在ほぼ一切存在しないので、布教兼ねて紹介したい。

プレイアブルの2キャラでクリア済み。クリア = Stage 3 のボス撃破としている。

Slay the Spire on Steam

https://i.gyazo.com/419ab928c2cc0060598b624a9c0d2f93.jpg

概要

基本的にはドミニオンのようなデッキ構築ゲーム。最初のカードはキャラクターごとに固定、毎ターン +3 Energy で 5 ドロー、Attack|Power|Skill のアクションカードをコストを払って実行する。

カードは敵を倒したりショップで購入することで手に入れる。他のデッキ構築ゲーと一緒で圧縮が重要で、イベントマスのストアで金払って1枚廃棄したり、特定のイベントで廃棄したりできる。休憩ポイントで各カードは一回だけ強化できる。

英語か機械翻訳の日本語で、簡単な英語なので英語でやればいいのだが、カード説明の Exhausted が 「その戦闘内での廃棄」に気づくのに時間がかかった。

良さ

最近は自称ローグライクが氾濫しているが、プレー感覚はしっかりとローグライクで、ビルド 最初から理想のゴールを決めて突っ走るか、その場で決めていくか、思い通りにいかない感じが絶妙に歯痒くてよい。

自分はこの手のゲームだと Faster Then Light が一番好きなのだが、プレーのリズム感は似ている。馴れてくると1プレー1時間ぐらいでテンポが良い。

キャラクター

The Ironcrad

戦士系っぽいイメージ。 初期から持ってる、戦闘終了ごとに HP6 回復のレリックが強くて結構無茶できる。

最初にクリアしたときは Felix(そのターン中のみダメージ+4) Heavy Blade(固定ダメージ+ダメージ増加倍率が3倍の追加ダメージ) 以外をドロー系で固めて、Felix をブン回すことで高倍率の Heavy Blade で最大ダメージを叩き出す感じだったが、二回目のクリアは Barricade(ガード値がリセットされなくなる) と Heavy Slum(現在のガード値だけダメージ)のビルドでクリアして、やる度に別のプレー体験になるように多様なビルドが可能。たぶんいちばん調整されている。

Corruption(全ての Skill カードが Exhaust になり、コストが0になる) が何やるにしてもとにかく強い。ただし入手確率も低い。強すぎるので調整されそうな気がする。

The Silent

毒使いのアサシン。脳筋よりビルドがピーキーで上級者向け。 Ironcradと違って回復のレリックがないので、慎重な立ち回りが要求される。

敵の得手不得手がはっきりしてるので、序盤のユニークに手も足も出ずにやられることもしばしば。毒ビルドは強いのだがダメージ出力が出るのが遅いので、その間に防御値を稼げずに負けることも多い。

結局ダガービルドでクリアした。ダガービルドは結構回さないとカード自体がアンロックされないので、それまでが辛かったが、Shiv のダメージ上昇バフを使って Shiv のドローに全部を賭けるビルドが完成したときは強すぎて負ける気がしなかった。

今後にも期待

さすがに2キャラは少ないのだが、それでも30時間ぐらい遊べたし、まだまだやり込めそうではある。自分は30時間ぐらいやった。 追加予定の3キャラ目や、追加のボス、レリックなどに期待しつつ、正式版まで追加要素追うだけでもニヤニヤできそう。

サクサク出来るデッキ構築ローグライクでリプレイ性も高い。おすすめです。

読まれるテキストは読者へのおもてなしの構造を持っている

大学生だった当時、梅田望夫の本を読んではてなにやってきた僕は、ブログ論壇への憧れだけがあって、技術者にもなれず、時流のテーマに対して書くべきテーマを持たず、ただ実家の宗教に対する恨みだけを書き綴っていた。

もちろん、そんなものを好きこのんで読む人はいなくて、ただ虚無へとテキストを放り込んでいたのだけだど、いつからか、ある程度パターンを獲得して、その真似をするようになって、成功失敗を繰り返して、それなりにPDCAを回してきたと思う。思えば、その過程でいろんな人のヘイトを買った気がする。

人間のテキストの読み方、その反応、というのはパターンを、いくつか書き起こしてみる。

読者は、ファーストビューのレイアウトで、読む読まないを決める

  • タイトルは記事の印象の5割
  • 章タイトルが残りの半分
  • 本文はほとんど読み飛ばされる
    • 書き手としては単語の印象の連なりでイメージを形成することになる
  • 段落が均等に分割されていると、テンポがいいと認識される
    • スクロールせずに次の段落の見出しが覗いてると尚良い
  • 中身の濃さに関わらず、スクロールバーの威圧感で「読み応えがある」と判断される

読者が求めているのは、そのリンクを開いたことに対する納得感である

  • 「釣る」ならどれだけ納得させるかが勝負になる
  • 結果として、強い主張 => それに対する想定反論へのエクスキューズ、という反復構造になる
  • 脳内に想定読者をエミュレートする。それは訓練で得られる。はてブユーザーはそのイメージを既に持ってるだろう。
  • たとえば、この記事につくであろう反論は「あいつまた中身がない記事を書いてる」「使い古された文章論/ソーシャルマーケティング論」「技術以外のことに言及すると残念」「瞑想と野菜が足りない」などである
  • それに対する反論として、僕は自分自身の納得感のためにブログを書いている、上からコメントすることで気持ちよくなれるなら勝手にどうぞ、僕は気にしないけど、といったスタンスであることを表明しておく。別に言う必要もないのだが
  • 同意を取りたい人に訴求して、叩きたい人には気持ちよく叩かせて、シェアしたい人にはその人のTLを彩るオシャレタイトルを提供する、のが書き手の役割

テキストの質は、読まれるかどうかに影響しない

  • 複雑な暗喩や係り受けの構造、レトリックが含まれるかは、それが読まれるかにほとんど関係ない
    • 書き手の自己満足といってもいい
  • 先に述べた段落構造のテンポ感だけで判断されがち

説明してもしすぎるということはない

  • 同じことを何度も書いて嫌がる人は少ない。なぜならほとんどの読者は読んでいないから
  • 読者は余計なメタ構造を求めない。記事単独で主張が簡潔するのが望ましい。
  • 引用先は読まれない。引用するとしたら読者がその記事を読んでいることを前提に記事タイトルを作る

強い言葉、エモさの役割

  • 人が記事を開いてるのは、何かを言いたいからであり、同意の対象、批判の対象を探していて、その端的なターゲット、「的」として引用したくなる「強い言葉」を散りばめておくと、その記事の反応を見る時にメタ構造がはっきりしやすい
  • ただし、強い言葉は、書き手の認知をも歪める。読者に訴求したいがためのエモいフレーズに引っ張られてて、本来言いたかったことと違うゴールに着地してることもある
  • それはそれで酔狂だと思っていて、自分はあえてそのままにしている

闇に飲まれるな

  • 批判を受けて暗黒面に落ちていった人は多い
  • 心を折らないために、無言の言及/シェアは全て自分への賛同だと思い込む。それぐらいじゃないと釣り合いが取れない。
  • 強い言葉による批判は、センセーショナルで、目につきやすい
  • というメタ構造を知ることで自分へ向けられた敵意を相対化する必要がある
  • 人格批判されるのは辛いけど、人格批判した側の方が品性を疑われてるはずと開き直る

言いたかったこと

俺は絶対に暗黒面に落ちないからな

というのを僕が尊敬する技術者でありブロガーである rui314 さんのツイートを見て思った次第です。

GUI環境でWeb の UIを構築する開発ツールを作りたい

qiita.com

という記事を書いた。 要約としては、「今こそGUIでWebのUIを生成できるような開発環境を目指すべき」みたいな話。

概念

Web の GUIの概念を分解すると

  • JS はViewとしての実装を持つプレゼンテーションレイヤー、ロジック注入のコンテナーレイヤーに分割して考える
  • HTML/CSS は、レイアウト定義のレイヤー、そのレイアウト領域を占めるエレメントのレイヤーに分割して考える

この考え方に基づくと、GUI開発環境で作るべきものは、

  • テンプレートへのJSロジックのつなぎ込み
  • レイアウトエディタ
  • 個別のエレメントの装飾

となる。

JSロジックのつなぎこみはヘヴィにJSだからまだコードで書く方がいい。作るならノードベースのエディタになりそう。 エレメントの装飾は自力で頑張るより Sketch から生成できないか試したい。後回し。

レイアウトエディタは今でも作れそう。作った。

レイアウトエディタを作った

css grid layout を用いて、GUIでポチポチやることで、レイアウト生成をある程度自動化する。

https://mizchi-sandbox.github.io/grid-generator/ で試せる。

見ての通り、非常に雑だが、css grid layout としての表現力は試すには一通り機能は揃ってると思う。

https://i.gyazo.com/fd5f6f75b5f577b18ec276c1655ff80d.png

サンプルで用意した HolyGrail レイアウトだと、こんな感じのCSSが生成される

.gridContainer {
  width: 100%;
  height: 100%;
  display: grid;
  grid-template-columns: 120px 4fr 1fr;
  grid-template-rows: 60px 1fr 40px;
  grid-template-areas: 'header header header' 'left content right' 'footer footer footer';
}
.headerArea {
  grid-area: header;
}
.leftArea {
  grid-area: left;
}
.contentArea {
  grid-area: content;
}
.rightArea {
  grid-area: right;
}
.footerArea {
  grid-area: footer;
} 

この辺命名ルールがプロジェクトごとに違うので、うまい感じで表現するのが難しい。とりあえず適当。 注意点として、IE11 で動かすには postcss の autoprefixer などを通す必要がある。

作ってみた感触

まだPoCレベルの品質だが、レイアウト定義ツールとして発展性がある土台が作れたので、あとは欲しいものを気合で作っていくとどうにかなりそうな気がする。 2次元空間の連結処理を愚直に書いてると、まるでゲームプログラミングのようだった。こういうの辛いけど割と好き。

TODO

  • メディアクエリによるレスポンシブ対応
  • ドラッグで範囲指定で連結
  • 指定したgrid-areaをさらに子コンポーネントとして分割できるようにする
  • 指定したgrid-area へ React Component を埋め込んでプレビュー
  • 生成コードにpostcss通しておく
  • Atom プラグイン

思いつく限りだとこんな感じだろうか。 本気で作るなら patreon とかで gittip 使うべきか。あとで考える。

エンジニアのベンチャー企業の選び方/働き方/やめ方

この記事は退職者アドベントカレンダーの12日目です。

adventar.org

経歴としては、新卒で設立してすぐのゲーム会社 => 小規模教育系ベンチャー => Incements(Qiita) => フリーランス

今年で29歳、20代で3回退職しました。20代のうちは冒険してベンチャー企業で働いてみよう、と思ってたのですが、結局29を目前にフリーランスになってしまいました。

ベンチャーで働くこと

ベンチャーで働くのはリスクを取るということ。一番言いたいのは、ストックオプションもたずにベンチャーやるな、ストックオプションも確実に換金できるわけじゃない、ということ。上場するときに行使するか、バイアウト時に買い取ってもらわないといけません。

また、ストックオプションの期待だけ給与は下がるので、他の会社で同じことをやるのに比べて、 -100~-150万ぐらいの相場です。少数精鋭志向で最初からじゃんじゃん金がつぎ込まれてる会社はその限りではないですが、入るのも難しいですね。

つまりはストックオプションなしのベンチャーは、よっぽど給与が高くないと1つの搾取の形態です。若者の夢を食って成長する会社、残念なことにいっぱいあります。気をつけてください。

ただし、給与テーブルというものがないので、新卒でも即戦力ならお金は出るので、一律350万スタートみたいな大企業と比べると、最初のキャリアとしての給与は良いかもしれません。というか研修などはないので、扱いは中途です。

ビジョンとそのズレ

ベンチャー企業は、言ってしまうと創業者の夢を叶えるためのもので、ビジョンに共感して入社したとしても、些細な方向性の違いがどんどん大きくなるし、またそれが実現したところで実はお金にならなかった、みたいなことも往々にしてあります。

自分がプロダクトに思い入れを持つのはいいことなのですが、持ちすぎると会社が自分の願いに反する方向に舵をきったととき、裏切られたという感じてしまうこともあります。

共感は一つのモチベーションの作り方ですが、まったく共感をせずとも働けるのならそれは1つのやり方としてアリです。とくに技術者の場合は、目先で使う技術だけ意識してれば後はあんまり気にしない、というのもアリでしょう。ただし、その場合はその技術の使うアテがなくなったタイミングで辞めることになるでしょう。

ベンチャーの選び方

とくに小さいベンチャーでは、経営者は会社そのものです。経営者がやりたいことと、自分がやりたいことが合致するか、それが最初の確認ステップです。

自分は、会社に対して労働という形態で会社に投資をする形になります。その見返りはストックオプションです。

上場は簡単にはできません。ほとんどはそこにたどり着かないし、最短でも創業から4~5年、うまくいって10年かかるものです。自分の人生のうち10年をそこに賭けられるか?と考えてみてください。

幸いなことに、この業界は流動性が高いので、言い方が悪いですが「損切り」も簡単です。

自分がベンチャー企業をやめた理由

僕は結構プロダクトに思い入れるタイプで、作ったものに愛着とかを持ってしまうんですが、それが自分の願ってた方向と違う方向で使われたり、自分があるべきだと思った姿を歪められてしまうと萎えてしまう、ってのが1つ。

もう一つ、なんだかんだで技術そのものに興味があり、興味がある技術を常に追い求めていても、ベンチャーという小さい組織では使うアテが見つからない、ということです。次期社内フレームワークの選定のディスカッションでレガシーな方向にいって萎えたり、ということもありました。

言い方は悪いですが、それが競争力の源泉ではない限り、技術的にリスクをとらなくてもいい、というのがベンチャー企業の1つの在り方です。もちろん、あるに越したことはないので、みんないい技術者がほしい、といいますが、現場では必要とされなかったりします。ベンチャーに限らず、採用あるあるですね。

また、僕自身の属性として、若手から中堅どころになり、ベンチャーで給与テーブルをハックしなくても給与が上がる年齢になった、というのもあります。悪い慣習ですが。

ベンチャーを経験して、なぜ今フリーランスやってるのか

行きたい会社がないです。

1つに僕の専門としているフロントエンド技術が、長期的に活用できる会社がない、というのがあります。国内のウェブ業界で、技術寄りに倒して R&D に近いレイヤーでそれができる会社は、売上的に余裕がある必要があり、また日本のITベンチャーで金銭的な余裕があって競争力がある会社の選択肢があまりなく、それらとマッチしません。

国内で外資だとGoogleは多少興味があるんですが、どっちかというと中身を見てみたいという好奇心であって、僕がCSの基礎にスキル振ってないので、基礎教養だったり英語だったり、その辺が明らかに欠けてる認識あるんであんまりマッチしておらずお呼びでないという気がしていて、また自分としても今行くべき会社でもないという感じを察しており、今はあくまでGoogle技術の利用者という立場の方が気が楽です。

海外は…どうかなぁ。あまり国内から出たいという気持ちがないです。シリコンバレーの基準の給与はほしいですね。5000兆円ほしい。

今は、短期で小さい仕事を、難しい分ちょっと単価上げて取るほうが、僕自身のためにも、社会のためにも、全体効率としては良い、という時期だと認識しています。

いい話があったらお待ちしております。

フリーランスどうだったか

最初は不安だったけど、なんだかんだで仕事は断らないといけないほどあることがわかりました。

売上も結構立ったので余裕があって、来年は仕事減らして新しいことを勉強しようと思っていて、何を勉強するか考えてる最中です。ブロックチェーンとか気になってます。個人でスモールスタートなビジネスを、思いつきでやる分には一番近そうだし、まあ個人で何かやらなくても需要が高い技術になりそうなので。

まとめ

  • ビジョンはモチベーションの源泉だけど、それが過ぎると辞める理由にもなる
  • やる気ある若者はベンチャーで給与テーブルをハックできる
  • 僕は今行きたい会社がない

なんだかんだでコードを書くのが好きなので、それをやらせてくれる環境に居続けるために、色々やらないといけないですね。終わり。

当初の懸念どおりブラウザのプッシュ通知は邪悪に使われはじめている。実装側はクリックまで購読確認を待つべき。

プッシュ通知、ネイティブアプリの機能郡をWebに持ち込むPWA技術の売りの一つだが、当初から懸念されていたとおり、非常にノイジーなものとなってしまっている。自分も気づけばあらゆるサイトの購読確認を、無意識で拒否を押すようになってしまった。

hagex.hatenadiary.jp

少し前の記事。最近はどこかで wordpressプラグインになったのか、目にする機会が非常に多くなり、非常にストレスフル。最初は技術的な目新しさからか、ある程度容認していたが、さすがにこの状況が悪化する一方で、本気でやばいんじゃないかと思っている。とくに初見のブログの記事を読む前に、購読確認が出るのが最悪の体験となっている。

そもそもプッシュ配信とはそういうものであり、運営側はグロースハックの名目で何が何でもユーザのリテンションを獲得しようとしてくる。と考えるとプッシュ機能は性善説に立って設計して良いものではない。最近はユーザーの認知もある程度キャズムを越えたのか、悪評ばかりが集まるようになっている。また Chrome側の通知管理UIもよくわからないところにある。

実装の話

どこもかしこもこんな風に初期化時に購読を求めにいく。

const serviceWorkerRegistration = await navigator.serviceWorker.ready
const subscription = await serviceWorkerRegistration.pushManager.getSubscription() // ここで確認モーダルが出る

自分が言いたいのは、要はなにかしらのボタンをクリックして初めてモーダルを出してくれという話

document.querySelector('.subscription-button').addEventListener('click', async ev => {
  const serviceWorkerRegistration = await navigator.serviceWorker.ready
  const subscription = await serviceWorkerRegistration.pushManager.getSubscription()
  // ...
})

Googleの人やWeb標準に人にいいたいのは、そもそも仕様レベルでクリックをイベントのルートに持たないと購読できない、としたほうが良いのではないか。すでにそういう実装になってるAPIとして Fullscreen APIがある。

https://developer.mozilla.org/ja/docs/Web/Guide/DOM/Using_full_screen_mode

忖度

この機能の背景として、Webの相対的な立場の低下を踏まえ、モバイルアプリを倒すために今現在モバイルアプリが(提供側が)一番好んでいるであろうプッシュ通知をブラウザ側にもってきて、モバイルの体験をWebにもってきたい、というのはよくわかるが、ここでユーザーの体験のことは無視されがちだ。ちゃんと機能すれば良いもの、という言い訳もわかる。が、現状最悪な方向に向かっている。

この状況が続くと、2003年ごろjsをみんなオフにしていたように、ユーザー間でServiceWorker をオフにすることを推奨されたり、PWAというブランディングそのものを毀損してしまう恐れがある。

追記

Google の アドボケーターの agektmr さん曰く

自分でコードを書きながらブロックチェーンを勉強した

マネーゲームとしての仮想通貨は興味はないのだが、技術的に興味があって自分で簡単なコードを写経しながら勉強した。

定義

  • ブロックチェーンの実体はブロックを繋いだリスト構造
  • ブロックはいくつかの入力値(生成日時など)と、自分自身のハッシュを持っている
  • 前のブロックのハッシュ値と、入力値を元に自分自身のハッシュが決まる。その手順は公開されている。

要はハッシュ値とそのメタデータが連続するただの配列なりの LinkedList。面白いのはここから。

  • ネットワークに参加するそれぞれが任意に新しいブロックを追加することができる
  • ブロックチェーンは検証結果が正しく、より長いものが信頼される
    • なのでビットコインみたいな仮想通貨は、生成コストが重く、検証コストが軽いものが好まれる。
  • 他のネットワーク参加者からブロックチェーンの更新を受け取った時、手元のブロックチェーンとそれを比較し、より長いものを自分のブロックチェーンとして受け入れる
    • 同じ長さのときは何もしない。将来的に自分のブロックチェーンを信頼させるか、新しいブロックチェーンを受け入れて破棄するかが起こる。
    • 勝手に検証ロジックを変更したネットワーク参加者がいたとしても、それは他の参加者から受理されない。ただしネットワーク参加者間で合意がとれれば、新しいブロックチェーンに分岐することはありうる。

ここでいうネットワーク参加者は、P2Pでもなんでもいい。

https://github.com/lhartikk/naivechain/blob/master/main.js を参考にしながら、 JS (babel + flowtype) でアレンジしながら写経した。

/* @flow */
import SHA256 from 'crypto-js/sha256'

type Block = {
  index: number,
  previousHash: string,
  timestamp: number,
  data: string,
  hash: string
}

type BlockChain = Block[]

/* initial block chain */
export function getGenesisBlock(): Block {
  return createBlock(
    0,
    '0',
    1465154705,
    'my genesis block!!',
    '816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7'
  )
}

export function getLatestBlock(blockchain: BlockChain): Block {
  return blockchain[blockchain.length - 1]
}

/* implementation */

export function createBlock(
  index: number,
  previousHash: string,
  timestamp: number,
  data: any,
  hash: string
): Block {
  return {
    index,
    previousHash,
    timestamp,
    data,
    hash
  }
}

export function calculateHashForBlock(block: Block) {
  return calculateHash(
    block.index,
    block.previousHash,
    block.timestamp,
    block.data
  )
}

export function calculateHash(
  index: number,
  previousHash: string,
  timestamp: number,
  data: string = ''
): string {
  return SHA256(index + previousHash + timestamp + data).toString()
}

export function generateNextBlock(
  blockchain: BlockChain,
  blockData: string = ''
): Block {
  const previousBlock = getLatestBlock(blockchain)
  const nextIndex = previousBlock.index + 1
  const nextTimestamp = ~~(Date.now() / 1000)
  const nextHash = calculateHash(
    nextIndex,
    previousBlock.hash,
    nextTimestamp,
    blockData
  )
  return {
    index: nextIndex,
    previousHash: previousBlock.hash,
    timestamp: nextTimestamp,
    data: blockData,
    hash: nextHash
  }
}

export function isValidNewBlock(
  newBlock: Block,
  previousBlock: Block
): boolean {
  if (previousBlock.index + 1 !== newBlock.index) {
    console.log('invalid index')
    return false
  } else if (previousBlock.hash !== newBlock.previousHash) {
    console.log('invalid previoushash')
    return false
  } else if (calculateHashForBlock(newBlock) !== newBlock.hash) {
    console.log(
      'invalid hash: ' + calculateHashForBlock(newBlock) + ' ' + newBlock.hash
    )
    return false
  }
  return true
}

export function isValidChain(blockchainToValidate: BlockChain): boolean {
  // check genesis block
  if (
    JSON.stringify(blockchainToValidate[0]) !==
    JSON.stringify(getGenesisBlock())
  ) {
    return false
  }

  const tempBlocks = [blockchainToValidate[0]]
  for (var i = 1; i < blockchainToValidate.length; i++) {
    if (isValidNewBlock(blockchainToValidate[i], tempBlocks[i - 1])) {
      tempBlocks.push(blockchainToValidate[i])
    } else {
      return false
    }
  }
  return true
}

export function addBlock(blockchain: BlockChain, newBlock: Block): BlockChain {
  if (isValidNewBlock(newBlock, getLatestBlock(blockchain))) {
    return blockchain.concat([newBlock])
  }
  return blockchain
}

(こういうのこそテストコード書いたほうが良さそう…)

とくに難しいコードはない。 見るべきは検証部分の calculateHashForBlock(newBlock) !== newBlock.hash だろうか。その中身は今回はSHA256を計算しているだけ。

このコードを簡単に動かす。

/* @flow */
import {
  getGenesisBlock,
  generateNextBlock,
  getLatestBlock,
  addBlock,
  isValidChain,
  isValidNewBlock
} from './index'

// 初期ブロックチェーンを生成
const blockchain = [getGenesisBlock()]

// ブロックを一個生成
const prev = getLatestBlock(blockchain)
const next1 = generateNextBlock(blockchain, 'foo')
const newBlockchain1 = addBlock(blockchain, next1)

// もう一個生成
const next2 = generateNextBlock(newBlockchain1, 'bar')
const newBlockchain2 = addBlock(newBlockchain1, next2)

// 今のブロックチェーンが妥当か検証
console.log('current', newBlockchain2)
console.log('isValidChain', isValidChain(newBlockchain2)

これだけだと面白くないので、ローカルに競合する仮想的なネットワーク参加者を作って走らせてみる。

/* @flow */
import {
  getGenesisBlock,
  generateNextBlock,
  getLatestBlock,
  addBlock,
  isValidChain,
  isValidNewBlock
} from './index'
import range from 'lodash.range'

const blockchain = [getGenesisBlock()]

const wait = n => new Promise(resolve => setTimeout(resolve, n))

// これは全員がシングルトンのバッファを持つのは実装的によくないのだが、簡単なので…
let receivedBlockchain = [getGenesisBlock()]

// receivedBlockchain を更新するブロードキャスト関数
const bloadcast = (name, next) => {
  console.log(`${name} bloadcasted`)
  receivedBlockchain = next
}

const createMiner = (name: string) => {
  // Miner自身のブロックチェーンを用意
  let myBlockchain = blockchain.slice()

  // receivedBlockchain を信用するかどうか
  const accept = () => {
    console.log(
      `${name} checks receivedBlockchain. Size: ${receivedBlockchain.length}`
    )

    if (receivedBlockchain.length > myBlockchain.length) {
      if (isValidChain(receivedBlockchain)) {
        console.log(`${name} accepted received blockchain`)
        myBlockchain = receivedBlockchain
      } else {
        console.log('Received blockchain invalid')
      }
    }
  }

  // 自分自身でブロックを追加
  const addNewBlock = () => {
    const next = generateNextBlock(myBlockchain)
    myBlockchain = addBlock(myBlockchain, next)
    console.log(`${name} add new block: ${next.hash}`)
  }

  // ランダムな時間 wait しながら 0~2 個のブロックを追加
  // ランダムなのでどこかで競合して巻き戻ったりする
  return async () => {
    while (true) {
      await wait(Math.random() * 3000)
      accept()
      range(~~(Math.random() * 3)).forEach(_ => addNewBlock())
      bloadcast(name, myBlockchain)
    }
  }
}

// 3人ほど走らせてみる
createMiner('miner1')()
createMiner('miner2')()
createMiner('miner3')()

実行結果

...
miner2 checks receivedBlockchain. Size: 15
miner2 add new block: 5f9f37ce0a84dd148138e733ea63630c4c8482f99362aac7c23463b7594e02f9
miner2 bloadcasted
miner1 checks receivedBlockchain. Size: 16
miner1 accepted received blockchain
miner1 add new block: 2b2594d6cc92e3a25f5f567a645b81c0894e5271482440a4b8cdb2655f195553
miner1 add new block: 1e1018d769aa5a47b0b62e9176c8332e2e475869b43b062efcc31ace9fb1d1fb
miner1 bloadcasted
miner1 checks receivedBlockchain. Size: 18
miner1 bloadcasted
miner1 checks receivedBlockchain. Size: 18
miner1 add new block: 861633faa0478234004d0557ddabc1946892f2ad1f0474a90334a118842d708d
miner1 add new block: b5ba3acce6d6dc09c26808ddee6ad8ee82ad99bcb0bae148e41a786d03d157e9
miner1 bloadcasted
miner3 checks receivedBlockchain. Size: 20
miner3 accepted received blockchain
miner3 add new block: dc5d8959934dfe7843a5d263eb8d34dca8bd2a301d8471d06315aa667ca6db2f
miner3 bloadcasted
miner3 checks receivedBlockchain. Size: 21
miner3 add new block: 18fd2d81753d1b12c8ee4af1ef7623810cc9ed33d7e20353f4577c8ba9def2e5
miner3 bloadcasted
miner2 checks receivedBlockchain. Size: 22
miner2 accepted received blockchain
miner2 add new block: 304f91a4ee6b10f022988a22a07b5724139a4023dfcd7cd72f50be0452962ccf
miner2 bloadcasted
miner2 checks receivedBlockchain. Size: 23
miner2 add new block: d91a1ca6137dce1b6cb2fd5c2a087e75193568d9a6d5f675c37cf85542edeaca
miner2 add new block: 0fe784699ff2b63b3affaccee27d962637fbd1aedbb50dcf2892bbddcd00d154
miner2 bloadcasted
miner3 checks receivedBlockchain. Size: 25
miner3 accepted received blockchain
miner3 add new block: 577f63f1a5093cffe4e8ce086a8977105bb5d457f7b9424160a03883921365e3
miner3 add new block: ccf166e561e75cf865e7314105f098b022116977061ad131c253a1fb03766d15
miner3 bloadcasted
miner2 checks receivedBlockchain. Size: 27
miner2 accepted received blockchain
miner2 add new block: 9b69c13ecc83b5d2216fbede77e0d1ceeb634f12ec237fe7b38d2c363b4091ff
miner2 bloadcasted
miner1 checks receivedBlockchain. Size: 28
miner1 accepted received blockchain
miner1 add new block: 953cdde0848bb5ec9a727638383f680d5507759ed4496cab908d34e154d9676f
miner1 add new block: 37b295ad1b9ccd1de70eeb0292a84dfa67263cbf1513e21f2aaec2b8ec59fc08
miner1 bloadcasted
miner3 checks receivedBlockchain. Size: 30
miner3 accepted received blockchain
miner3 add new block: fe47c3f3c892b4cfa6954322d0203751d7d9dea4eb8431513c259165772a2284
miner3 bloadcasted
miner2 checks receivedBlockchain. Size: 31
miner2 accepted received blockchain
miner2 bloadcasted
miner2 checks receivedBlockchain. Size: 31
miner2 add new block: 44a718dc29699a4599b79b5b6cc21eef335699dd8fc1439696402725e3d5cedb
miner2 bloadcasted

まあなんか動いてそう。 本当はちゃんと p2p で動かしたり、敵対的な嘘の検証を行ったり publish する miner とかを混ぜてみたかったけど面倒なので略。あとで気が向いたらやる。