Edge Worker PaaS の fly.io が面白い

なかなかよいおもちゃを見つけたので、紹介します。

fly.io

fly.io は CDN Edge Worker で JavaScript に特化した PaaS です。既存のサービスで近いものだと CloudFlare Workers もしくは Lambda@Edge でしょうか。

アカウント登録をして、次のようなコマンドを叩くとエッジで動くアプリケーションを作成することができます。

npm install -g @fly/fly
fly login
# mkdir my-flyio; cd my-flyio
fly new

最小コードはこんな感じ。CloudFlare Workers と同じような ServiceWorker 風と、Google Cloud Function 風の 2 つのパターンでワーカーを定義できます。

// index.js
addEventListener('fetch', function (event) {
  event.respondWith(new Response('Redirecting',
    {
      headers: {
        'Location': 'https://fly.io/docs/apps/'
      },
      status: 302
    }
  ))
})

fly.io のここがいい

フロントエンドの最適化の行き着く先の解の一つ、それは「CDN Edge で HTML をキャッシュして返却する」というものです。そして、ある程度動的な JS をユースケースに応じて返却したい場合、 Edge で SSR を行って、一度実行した HTML はキャッシュする、という戦略が有効です。

JS の SSR は、残念ながら今後も必要な技術だと想定されています。しかし、コンテナ技術の発展で、Node の運用の敷居は前より下がりましたが、それでも Node を運用するのを躊躇するエンジニアの方も多いでしょう。

SSR や HTML Cache に対するこれらの懸念を、Edge Worker はまとめて払拭できます。

  • 必然的にPaaS なのでフルマネージド
  • レスポンス最適化のために Edge Cache で HTML を返せる
  • SSR は複雑な処理なので、ほぼフルスペックの Node がいる必要がある

また、SSR / BFF 特有の事情として、次のような要求があります。

  • HTML をキャッシュする場合、セッション情報に触れないほうが安全に SSR できる
  • SSR の主な需要は大雑把なファーストビューの最適化と、 GoogleBOT 向けの情報を構築することなので、セッションに触れる必要はない
  • 起動後にリバースプロキシとして動いて API オーケストレーションを行う

イニシャルビュー最適化で、すべてのリクエストに対してセッション情報付きで SSR するのを要求する人もいますが、運用事故を避けたり、キャッシュ最適化やパフォーマンスチューンまで考えると、この辺が現実的な落としどころだと思います。

つまりは BFF (Backend for Frontend) と呼ばれていた層の一部を地理的に最適化される Edge Location に持ってくることができます。

そして、キャッシュを扱う以上、それなりに KVS を扱いたいわけで、他のサービスではなく fly.io が優れてるのは、この辺のために便利なパーツが一通り揃っていることです。

  • @fly/cache: リージョン単位の揮発する KVS
  • @fly/cache/global: 永続化されるグローバルな KVS
  • @fly/db: 永続化されたグローバルな簡易ストレージ(Mongo 風?)
  • @fly/static: 簡易アセットサーバー
  • @fly/fetch/mount 簡易ルーター
  • @fly/fetch/proxy 簡易リバースプロキシ
  • varnish surrogate key のような、タグによる Cache Invalidation

varnish surrogate key による Cache Invalidation が個人的に一番欲しかったやつで、現状他の CDN だと fastly ぐらいしかこの機能を持っていませんでした。

https://book.varnish-software.com/4.0/chapters/Cache_Invalidation.html

各種ストレージを使ってみた感じ、それぞれは本格的なものではないのですが、アプリケーションサーバーへ到達する前の エッジロケーションにこれがある、ということが大事で、とくに各種キャッシュと簡易アセットサーバー、簡易ルータがあるので、頑張れば Edge で完結するアプリケーション層として動くことすら可能です。

作ってみた

というわけで、簡単な掲示板アプリを作ってみました。各ページはその Edge ではじめてときだけ SSR され、それ以降はキャッシュされた HTML を返却します。初期化以降は CSR で動きます。

https://grave-crowd-822.edgeapp.net

ソースはここ

https://github.com/mizchi/flyio-playground/tree/master/examples/react-ssr

主に React + react-router + styled-components での SSR と、 fly/db に書き込みの内容を永続化して、 fly/cache で SSR 済み HTML をキャッシュしています。next.js などは使わず手作業で無理矢理 isomorphic にしてるので、コードの見通しは悪いです。あとでどうにかする…

作ってみた感想

  • node エンジニアなら特に迷うところサッと作れる
  • ローカルでほぼサーバーと同等のものが動くので開発が楽 (lambda/cloud function はここが微妙)
  • GitHubにソースが公開されてるので最悪これを自分でホスティングすればいいという安心感がある
  • express (node http) 互換ではなく、コールバックが (req, res) => {} ではなく、 (req) => new Response(...) みたいな形式
  • API ドキュメントがまだまだふわっとしている
  • 独自の v8 でビルドされていて、微妙に実装されてないものがある (crypto 周りで動かないモジュールがあった)
  • fly/db は、本当にただのおもちゃなので、信用しないほうがいい… (query や scan 相当のものもないし、列挙も出来ない…)
  • 本番環境での fly/cache/global への書き込みはそれなりに遅い

複雑なストレージを持つようなものではないので、これ一つで完結する、というものではありません。

参考になるリソース

examples 眺めながら、公式ドキュメントを読みつつ、わからなかったら GitHub で実装を読む、という感じでだいたいやりたいことをやれました。

料金

  • $25 の無料枠
  • 有料の場合、最低月 $10 から

https://fly.io/docs/#pricing

超大規模想定で適当に試算してみても $100 超えるのは難しそう

で、本格的に使っていいの?

ぶっちゃけリリース直後なので、なんとも言えません。人柱募集したいところ。

とりあえずは、Edge Worker 使った際のアーキテクチャを考察するための実験場として使うのがいいんじゃないでしょうか。ユーザーが増えて、実質 SLA どのぐらいか判明したら、プロダクションに突っ込むかどうか考えれば良さそう。

cloudflare も Edge Worker というよりは Serverless として Edge Worker 市場に参入するぞ!みたいなことを言い出してるので、こっちも注視したいです。

https://workers.dev

Node.js エンジニアの活路としての Edge Side

前々から思っていたこととして、フロントエンドとサーバーサイドの間に、 エッジサイドのエンジニアという職域がうまれるんじゃないかと想像しています。先に述べたように API オーケストレーション+SSR を行う Node BFF がここに位置するのではないか、と思っています。

日本においては node.js エンジニアは(SFなどの海外と比べて)あまりサーバーサイドでシェアを取れませんでしたが、まともな ORM がない Node 一本で行くのは僕もちょっと厳しいと思っていて、Node が本当に生きるのは、ブラウザと緊密に連携する前段の、Edge Side なんじゃないかと考えています。

Edge Workers は単なるパフォーマンス要件を満たす選択だけではなく、BFF ベストプラクティスとしての活用があると思っています。SPA を何年も作ってきましたが SPA をフルに運用するには、やはりフロントエンドエンジニアが握る BFF が不可欠だという気持ちがあります。

という感じでみなさんも fly.io で遊んでみてはいかがでしょうか。

ちょっと前に Edge ヘヴィーなフロントエンドのアーキテクチャの考察をしたので、こちらもどうぞ

光を超えるためのフロントエンドアーキテクチャ - Speaker Deck

Kaggle Titanic やってみた感想

データサイエンスの一連の流れってどんな感じなんだろう?と体験して見るために、Kaggleのチュートリアルをやってみた。

https://www.kaggle.com/c/titanic

Kaggle Titanic は、Kaggle のチュートリアルでよく使われる題材。タイタニック号の乗客名簿と、生存できたかを含むデータを与えられ、予測モデルを作成し、その精度を競う。

データをダウンロードする

  • https://www.kaggle.com でユーザー登録をする。
  • https://www.kaggle.com/<username>/account にアクセスし API => Create New API Token から kaggle.json をダウンロード
  • ダウンロードしたアクセストークンを ~/.kaggle/kaggle.json に置く

以下 pipenv を使った例

$ pipenv install kaggle
$ kaggle competitions download -c titanic

これで train.csv と test.csv のデータが手に入る。test.csv は解答用で、生存できたか(Survived)のデータは含まれない。

大雑把な流れ

  • 欠損値の処理(今回は単に捨てた)
  • 値の正規化(年齢など)
  • train.csv のうち 80% を訓練用、20% をテスト用データとして分割
  • 訓練モデルを作成
  • 訓練モデルにテストデータを入力し、精度を測定

今回は kaggle への提出は行っていない。

keras によるモデル作成

https://github.com/linxinzhe/tensorflow-titanic/blob/master/keras_titanic.py を参考に、理解するためにリファクタしてみた。

from keras.optimizers import SGD, Adam
from keras.layers import Dense, Activation
from keras.models import Sequential
from sklearn.preprocessing import StandardScaler
from sklearn.preprocessing import LabelEncoder
import numpy as np
import pandas as pd


def normalize_data(data):
    not_concerned_columns = ["PassengerId", "Name",
                             "Ticket", "Fare", "Cabin", "Embarked"]
    data = data.drop(not_concerned_columns, axis=1)
    data = data.dropna()

    # normalize
    dummy_columns = ["Pclass"]
    for column in dummy_columns:
        data = pd.concat([data, pd.get_dummies(
            data[column], prefix=column)], axis=1)
        data = data.drop(column, axis=1)

    # normalize Label:Sex to int
    le = LabelEncoder()
    le.fit(["male", "female"])

    data["Sex"] = le.transform(data["Sex"])

    # normalize Age
    ss = StandardScaler()
    data["Age"] = ss.fit_transform(data["Age"].values.reshape(-1, 1))
    return data


def split_train_and_test(data, rate=0.8):
    data_y = data["Survived"]
    data_x = data.drop(["Survived"], axis=1)

    train_valid_split_idx = int(len(data_x) * rate)
    train_x = data_x[:train_valid_split_idx]
    train_y = data_y[:train_valid_split_idx]

    valid_test_split_idx = (len(data_x) - train_valid_split_idx) // 2
    test_x = data_x[train_valid_split_idx + valid_test_split_idx:]
    test_y = data_y[train_valid_split_idx + valid_test_split_idx:]

    return train_x.values, train_y.values.reshape(-1, 1), test_x.values, test_y.values.reshape(-1, 1)


def build_model(input_dim):
    model = Sequential()
    model.add(Dense(20, input_dim=input_dim))
    model.add(Activation('relu'))
    model.add(Dense(1, input_dim=20))
    model.add(Activation('sigmoid'))
    model.compile(optimizer=SGD(lr=0.01),
                  loss='binary_crossentropy',
                  metrics=['accuracy'])
    return model


# load data
train_data = pd.read_csv("data/train.csv")
normalized_data = normalize_data(train_data)
train_x, train_y, test_x, test_y = split_train_and_test(normalized_data, 0.8)

model = build_model(train_x.shape[1])

# train
model.fit(train_x, train_y, nb_epoch=120, batch_size=16)

# test
[loss, accuracy] = model.evaluate(test_x, test_y)
print("loss:{0} -- accuracy:{1}".format(loss, accuracy))

だいたい 80~86%ぐらいの予測率だった。他のチュートリアルを見てもそのぐらいに収束するっぽい。 competition で100点だしてる人たちは、答えをなんやかんややってチートしてそう。

感想

  • あんまり綺麗なコード例が見つからない。今回の題材は、チュートリアルなのに、コードを綺麗に見せようという努力がなされたものを見かけなくてイライラした。
  • jupyter notebook でやる人が多いのか、数行のスニペット単位で整形することが多く、プログラマ的なモジュール分割ではない、という印象を受けた
  • 卒論でR触ってたのでpandas のデータフレームなんとなくわかったが、各種utility が覚えゲーっぽい
  • 今回は単に欠損値を捨てたが、中間値で埋めたり、それ自体に予測モデルを作って埋める人が見受けられた。中間値それ自体がバイアスになったり、あるいは欠損値があることそのものがある種の特徴量になってる場合、どう扱うべきか、コンテキストごとに迷いそう
  • 出先でやっていたが、バッテリ消費が激しい
  • 一応、このデータでは「女・子供は助かる可能性が高い」というのは知っていたのだが、ディープラーニングを使うとその事実に気づくことなくモデルができてしまうので、ドメイン的な学び甲斐がなかった。NNのバイアスから結果に響かない特徴量を検出して捨てる、みたいな手法はありそうなので調べる

実践: React Hooks

hooks が発表されてから趣味でも現場でもずっと hooks を使っています。おかげでだいぶこなれてきて、だいたいなんのライフサイクルでも表現できるようになってきました。

最初は単に useState が state を、 useEffect が componentDidMount/componentDidUpdate を置き換えるもの、と説明を済ますつもりでしたが、 useEffect についてはライフサイクルのモデルがぜんぜん違うので、別の説明をする必要があるように感じていました。

で、その結果 React Hooks を理解するには、関数のメモ化を理解するのが最も簡単だと思ったので、その説明をしつつ、イディオムを解説していこうと思います。

最初に: React Hooks は何であり、何ではないか

関数コンポーネントが状態を持てるようにするもので、関数のメモ化のテクニックを多用します。

redux を置き換えるものではない。が… Redux の機能を一部吸収しています(useReducer)。後述する Context と組み合わせることで、middleware なし Redux が簡単に実現できます。

直接的には recompose や HOC といったテクニックを置き換えるものです。

メモ化されたライフサイクル

(メモ化関数についての基本的な説明をするので、わかってる人は読み飛ばしてください)

const fib = n => n < 2 ? n : fib(n - 1) + fib (n - 2);
fib(10) // 55

この fib(10) は、計算の定義自体は簡単ですが、実際には何度も同じ値への計算をすることになります。 これを効率よく計算するため、計算済みの値は保存してしまうこととしましょう。

この fib 関数をメモ化するとこんな風になります。

const memo = [0, 1]; // js の配列のインデックス外へのアクセスの雑な挙動を利用
const fib = n => {
  if (memo[n] != null) return memo[n];
  const r = fib(n - 1) + fib (n - 2);
  return memo[n] = r;
}

これで、 fib(n) が計算済みだったら、計算済みの値を返す、という挙動になります。

ちなみに、 lodash の _.memoize を使って const fib = _.memoize(n => n < 2 ? n : fib(n - 1) + fib (n - 2)); でもメモ化できます。

React Hooks でのメモ化

React Hooks を活用するには、このメモ化の仕組みを理解しておく必要があります。

React Hooks の、特に useEffectuseCallback の第二引数は、このメモ化の理解を必要とします。

function Foo({x}) {
  useEffect(() => console.log('x changed'), [x]);
  return <div>{x}</div>
}

これは Foo への props x が書き換わるごとに、useEffect が実行されます。 useEffect(fn, memoizedKeys)fn は、 memoizedKeys ごとにメモ化されている、と言えます。ただし、全ての状態をメモ化しているのではなく、直前の状態だけをメモしています。

ここではメモ化のヒントは 配列になっています。一致判定のロジック自体は配列の各要素の shallow equal、雑に言ってしまうと newKeys.every((k, i) => k === oldKeys[i]) です。

差分検知に失敗するケース

useCallback は memoizedKeys でメモ化された関数を返却します。もし、 memoizedKeys が一致する場合、新しい関数は生成されません。前のステップで生成された関数をそのまま返却します。

x+y が表示され、クリックすると、x+y を console.log する、というコンポーネントで考えてみましょう。

function Sum({x, y}) {
  const onClick = useCallback(() => console.log('x,y changed', x + y), [x]);
  return <div onClick={onClick}>{x + y}</div>
}

このとき、新しい関数が生成されるのは、 x が変更されるときだけなので、y の更新には反応しません。なので、y だけ更新した際は、表示は更新されても、新しい x と 古い y を足した値を console.log することでしょう。

正しく両方の値に反応したい場合、次のように [x, y]memoizedKeys を与える必要があるわけですね。

function Sum({x, y}) {
  const onClick = useCallback(() => console.log('x,y changed', x + y), [x, y]);
  return <div onClick={onClick}>{x + y}</div>
}

より一般化すると、「関数クロージャの中で参照する要素はすべて memoizedKeys に列挙する」というベストプラクティスになると思います。

ちょっとむずかしいですが、総じて、差分アルゴリズムに由来する React らしい、一貫した副作用の抽象化と言えるでしょう。

実行コンテキストの仕組み

見た目上は魔法のように見える Hooks の記法ですが、単に副作用が外出しされているだけです。最初は algeblaic effects が云々みたいな話がありましたが、その話は忘れてください。

中で何が起こっているかは、すごく雑な疑似コードを書くと、たぶんこんな感じだと思います。

// 実行コンテキスト
const hooksMap = {}

  // ReactDOM.render の中の差分更新のどこか
  const hooks = hooksMap[oldElement.id] || []
  const newElement = updateElement(oldElement, virtualElement, hooks);
  hooksMap[newElement.id] = newElement.hooks;

hooks に実行インスタンスを引っ掛けてコンテキストを生成しているだけです。

より詳しい実装を知りたければ、こちらの翻訳記事を参照してください

(翻訳) React Hooks は魔法ではなく、ただの配列だ

イディオム

componentDidMount/componentWillUnmount 相当

import React, { useEffect } from 'react'
function Foo() {
  useEffect(() => {
    console.log('mounted')
    return () => {
      console.log('unmount')
    }
  }, [])
  return <></>
}

state を持つ

import React, { useState } from 'react'
function Foo() {
  const [state, setState] = useState([])
  return (
    <>
      {state.map(i => <div key={i}>i</div>)}
      <button onClick={
        ()=> setState([...state, Math.random().toString()])
      }>
        add
      </button>
    </>
  )
}

ref を使って DOM を触る。(textarea にフォーカスする例) DOMへ起きた副作用を検知する場合は useEffect ではなく useLayoutEffect になります。

import React, { useState } from 'react'
function Foo() {
  const textareaRef = useRef(null);
  useLayoutEffect(() => {
    if (textareaRef.current) {
      textareaRef.current.focus()
    }
  }, [])
  return <textarea ref={textareaRef}/>
}

外部の副作用を監視して、プログレスバーを進める、みたいな例

import React, { useState, useEffect } from 'react'
function Foo() {
  const [step, setStep] = useState(0);
  useEffect(() => {
     // 何か外部の副作用
     const unsubscribe = resouce.subscribe(() => {
        setStep(step + 1);
     });
     return () => unsubscribe() 
  }, [step])
  return <></>
}

useReducer + context = redux like

useReducer は react で公式に reducer の仕組みが取り込まれたものです。これは単に、state の reducer 版イディオムでしかありません。

これに、親子の間で暗黙に値を受け渡す Context API と、useContext の hooks を使うと、次のように redux 風の flux が再現できます。

import React, { useReducer, useContext, Dispatch, ReactElement } from "react";
import ReactDOM from "react-dom";

type CounterState = {
  count: number;
};

const initialState: CounterState = { count: 0 };

function reducer(state: CounterState, action: any) {
  switch (action.type) {
    case "reset": {
      return initialState;
    }
    case "increment": {
      return { count: state.count + 1 };
    }
    case "decrement": {
      return { count: state.count - 1 };
    }
    default: {
      return state;
    }
  }
}

// Container
const CounterContext = React.createContext<CounterState>(null as any);
const DispatchContext = React.createContext<Dispatch<any>>(null as any);

function App({ initialCount }: { initialCount: number }) {
  const [state, dispatch] = useReducer(reducer, { count: initialCount });
  return (
    <CounterContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        <Counter />
      </DispatchContext.Provider>
    </CounterContext.Provider>
  );
}

// Connected component
function Counter() {
  const state = useContext(CounterContext);
  const dispatch = useContext(DispatchContext);
  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </div>
  );
}

ReactDOM.render(<App initialCount={2} />, document.querySelector(".root"));

ただし、useReducer はだいぶ redux のそれと比べて、store 層が簡略化されています。middleware の仕組みは一切ありません。

reducer 定義自体は、単に (state, action) => state を守っていればいいです。 redux.combineReducers をヘルパとして使ってもいいですし、redux で使っていた reudcer をそのまま置き換えることも可能です。

とりあえず useReducer + Context で作ってみて、複雑な Middleware が必要だったら Redux を使う、という感じでいいんじゃないでしょうか。

どう適応するか

現状、表現力という点で、関数コンポーネントとクラスコンポーネントはほぼ同等です。

class MyComponent extends React.Component といったクラス記法は、おそらく推奨されなくなるのではないでしょうか。消えることはないでしょうが、推奨されない、といった雰囲気です。

React は関数型プログラミングの雰囲気が強いコアチーム、出自(facebook)、コミュニティなので、おそらく今後見かけるコードは hooks がメインになる気がします。

これは勝手な憶測ですが、React は、頭がいいであろう Facebook のエンジニアがメインターゲットなので、頭が良い人しかターゲットにしていない、というのがたぶんあって、僕はそれが好きではあるんですが、 Vue に初心者層をかっさらわれたのはそういう姿勢に問題があったんじゃないか、という気持ちもあります。

何にせよ、シンプルな一貫したアルゴリズムを理解していれば、すべてがうまくいく、という世界観で、自分はそこが気に入っています。

GIGAZINE が音声認識アプリのマニュアルを書いてくれた

gigazine.net

先日作ったこれです recording-studio.netlify.com

いつもだったらこういうの紹介しないんですが、GIGAZINEさんがRecording Studio(仮。なんか適当に名前をつける必要があった)の紹介記事を書いてくださり、アプリ紹介のついでで、僕が面倒で書かなかったアプリの使い方のドキュメントまで詳しく書いてくださっています。リテラシー高い人しか使えない状態だったので、ありがたいです。

自分で読んでみると、たしかにマイクのパーミッションとか開始・終了とか暗黙的でしたね…

いろいろ機能追加を考えてたんですが、これ自体は小さくまとまって完成してるので、ドキュメントを古くしないよう、新しくつくるにしても別ドメインにしようと思います。

ブラウザでマイク入力から書き起こしを行うツールを作った

Chrome でマイクからの音声を録音して、その音声認識で書き起こしも同時に行うツールを作った。

recording-studio.netlify.com で遊べる。

Chrome に搭載されてる Web Standard Proposal? の SpeechRecognition API を使っている。

developer.mozilla.org

Chrome のみだが、 PC Chrome だけではなく Android Chrome でも動作確認済み。

ブラウザをオフラインにすると動作しないので、このAPI の 中身はたぶん Google Speech to Text API だと思われる。

出力

録音したものは webm ファイルとしてダウンロードできる。認識されたテキストも、タイムスタンプ付きのプレーンテキストなので、適当にもっていって、ぐらいの気持ち。

クラウド音声認識してることを除けば、どこかにアップロードしてるわけではない。インメモリに貯めて、そのデータを吐いてるだけ。

実はFirebase Storage にアップロードする版も作ったのだが、音声をホスティングするサービスの見積もりをした結果、個人で運用できるものではない、という判断になった。自分専用の Podcast ツールとして使うかも。

将来性

Google Speech to Text API は基本的に有料APIだが、これを通して使うと無料。

Web 標準のプロポーザルの顔をしてるがが Web標準になる気はしない。これを開発できるプレーヤは大手ベンダに限られるので、これを標準と言い張るのは邪悪ムーブな気がする。モデルが公開されていれば別だが…

とはいえ、とりあえず無料で遊べる内に遊んどこうというモチベーション。

今 Web DB Press でインタビュー企画を持ってて、書き起こしを依頼してるんだけど、その助けになるかなぐらいの気持ちもあり、作った。

Markdown コードブロックの JavaScript を bundle して実行するエディタを作ってみた

ペライチの markdown のコードブロックをビルドして iframe の中で実行できる

https://markdown-code-runner.netlify.com で遊べる。

前々から作れるなと思っていたので作ってみた。3時間ぐらいかかった。

仕組み

  • monaco-editor でコード編集
  • remark で codeblock の AST を収集
  • rollup と rollup-plugin-virtual でインメモリ上に依存を構築して bundle
  • iframe でクロスドメイン制約を与えた状態で実行(ifram.sandbox="allow-scripts")

外部からの入力受け取れないし、そもそも自動実行できないのでコード実行とはいえセキュリティ要件はそこまで厳しくないはず

TODO

  • 実験的に React だけ特別扱いしてるが npm 対応したい
  • monaco の補完用の worker がうまく起動してないので修正する
  • browserfs 対応
  • TypeScrpit のコード診断
  • prettier 対応
  • worker 対応
  • というか https://markdown-buffer.netlify.com 上に実装

2018年良かった漫画・ゲーム

漫画

ドロヘドロ(1) (IKKI COMIX)

ドロヘドロ(1) (IKKI COMIX)

ずっとよみたかったが kindle 版が発売されたのと、また完結したということで

サッカーユースの話

イサック(1) (アフタヌーンコミックス)

イサック(1) (アフタヌーンコミックス)

江戸時代、日本の傭兵がヨーロッパに渡って、30年戦争の中で仇(?)を追う話。

やたら弱っちい宇宙人に侵略される話。浅野いにお苦手だったが、サブカルちらしてはいるもののいつもと違うサブカルちらしかただったので、読みやすかった

ブルーピリオド(1) (アフタヌーンコミックス)

ブルーピリオド(1) (アフタヌーンコミックス)

ヤンキーが美大受験する話。美大受験うんちく

SF。最近売れてる天国大魔境に雰囲気近いかも

ちまちま増えてきた異世界グルメもの。龍を捕まえて食べる話

ニュクスの角灯  (1)

ニュクスの角灯 (1)

文明開化直後の長崎の貿易商の話。全然関係ないが地元が舞台なので土地勘わかって面白かった

ゲーム

  • Slay the Spire
  • Oxygen Not Included
  • Caves of Qud
  • Into the Breach
  • Path of Exile
  • Lucah: Born of a Dream
  • ドキドキ文芸部
  • ゼルダの伝説: BoW

個人的なゲームオブザイヤーは Slay the Spire 。そういえばこの記事書いてから1年が経った。

mizchi.hatenablog.com

当時、全然無名だったが、思えば遠くまで来た。デッキ構築型ローグライクのフォロワーも増えてきて、これからまたジャンルとして一つ花開くんじゃないだろうか。

Into the Breach や Slay the Spire のような、ゼルダのようなAAAタイトルのオープンワールドの真逆というか、ある種のミニマリズムを突き詰めてメカニクスを探求するタイプのゲームが増えてきて、ワクワクしている。ボドゲからの逆輸入というか