詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Wordleの漢字バージョン「漢字ル」を作った

漢字ル

こういうやつです。ぜひトライしてみてくださいね。

本家の Wordle を知らない人向けに解説すると、四字熟語当てクイズです。指定された回数のうちに当てないといけませんが、試行のたびに文字が合っているかどうかを教えてくれるので、絞り込みながら推理していきます。 漢字ルの独特なところは、文字単体だけではなく、漢字を構成するパーツ単位でもヒントが出るところ。以下のように、「匕」「耂」といったパーツが(たとえば「老」に)マッチしているようなヒントでもって、漢字を想像していきます。

f:id:motemen:20220206150126p:plain:w300

以下は制作記です。

アイデア

自分も Wordle にそこそこハマったクチで、自然と自分の母語である日本語でこれを実現するとなるとどうなるかを考える。漢字を使うとなると文字種が多くて難しすぎるだろうけど、部首の情報を使えればヒントになるのでは? そしてアホくさくていいのでは? と思ったのだった。

とくに自分の場合は以前 めかぬか というものを作っていて、KanjiVG というプロジェクトの存在を知っていたので、これ使ったらできるんじゃないか? という目論見もあった。

検討

とはいえ実装する時間が潤沢にあるわけではないので、スキマ時間で検討だけ進めておく。

実装については、あの UI をゼロから作るのはあまり楽しそうではなかったし、世の中に Wordle クローンが溢れかえっているのでベースの実装があるんだろうと探してみて hannahcode/word-guessing-game を発見。これを使うことにする。いまは別の人に transfer されていそうだな。

漢字の部首情報には、漢字を構成する部首/偏旁のデータセット というまさに、というものがあったのでこれを使う。正確には部首ではなくパーツなんだけど、よりおかしさが増すだろうってことでそのまま。

四字熟語は Yojijukugo というページが網羅性高くてよさそうだった。全然膾炙してないものもあると見て、N-gram コーパス - 日本語ウェブコーパス 2010 によく出現するもののみに絞ることにした。(しかしこのデータはリリース後、使わないことにした。もっと色んな入力があるようだったし、すべての妥当な四字熟語を列挙すると大量になりそうだったから……。)

それ以外にこのゲームに特有の部分は漢字のパーツ単位での正誤判定だったが、これだけ手元で書いておいた。こんな感じ。

% ts-node sketch.ts
[ '冬虫夏草', '泰然自若' ]
{ radical: '泰', guessIndex: 0, solutionIndex: undefined }
{ radical: '三', guessIndex: 0, solutionIndex: undefined }
{ radical: '大', guessIndex: 0, solutionIndex: undefined }
{ radical: '氺', guessIndex: 0, solutionIndex: undefined }
{ radical: '然', guessIndex: 1, solutionIndex: undefined }
{ radical: '月', guessIndex: 1, solutionIndex: undefined }
{ radical: '犬', guessIndex: 1, solutionIndex: undefined }
{ radical: '灬', guessIndex: 1, solutionIndex: undefined }
{ radical: '自', guessIndex: 2, solutionIndex: [ 2 ] }
{ radical: '目', guessIndex: 2, solutionIndex: undefined }
{ radical: '若', guessIndex: 3, solutionIndex: undefined }
{ radical: '艹', guessIndex: 3, solutionIndex: [ 3 ] }
{ radical: '右', guessIndex: 3, solutionIndex: undefined }
[ 'absent', 'absent', 'radical_correct', 'radical_correct' ]

正解「冬虫夏草」に対して「泰然自若」は、 がそれぞれ位置まで含めて正解。これはバカゲーの予感。

実装

で、ある晩まとまった時間が取れたので試しに作ってみることにした。

まずは頻出四字熟語を生成する。こういう処理は書き捨てになりがちで再現性がないことが多いが、かといってわざわざリポジトリに置く気もしない、と思って Colaboratory でやってみたところ結構よかった。

漢字の入力については、Pokedle がたぶん 見えない <input> で入力させる方法を取っていそうだなというのを見ていたので、同じような方法を取った。見た目にこだわりすぎると時間が溶けるので気にしない方向で適当な場所に置いておく。漢字変換して Enter でなんかするの、絶対ハマるだろうな~と思って調べたらやっぱりハマりそうだった。form の onsubmit を使うことにしたら回避できそうだったのでそのようにした。

部首の一致を表示するために、普通のフォントではなく KanjiVG で文字を描画したい。これについては過去の自分が めかぬか で実装していたのでこれを拝借。

このくらいがオリジナルの実装で、あとはフォーク元に乗っかっている。全体的にもとの実装が TypeScript/React であったため、変更したい場所を特定するのが容易だったし、他言語への対応など考えられているためかごく一部の変更で済んだため結果的にかなりスピードが出せて、その晩のうちに公開までこぎつけられた。


とまあ色んな人の仕事に乗っかったおかげでスムーズに制作できた。KanjiVG が使える CDN があったらさらに早く実装できただろうな~。自分で作ってても相当難しいと思ったが、意外とプレイしてもらえていて、かつ解けている人も多くて驚きがある。

リポジトリはこちらです。

GitHub - motemen/kanjile: A word guessing game made using React, Typescript, and Tailwind

Alfredの代替としてRaycastを使っている

新春ツール入れ替えシリーズです。macOS における Spotlight 的なランチャーツールとして Alfred を長いこと使ってきたが、最近 Raycast を使ってみてこれがよかったので、以来ずっと使い続けている。

Raycast - Supercharged productivity

開発者のための便利ツールという売り文句のようで、そういう点がまさに気に入った。

カレンダーの次の予定が表示される

まずこれがいい。これだけで十分使える。ランチャーを起動したときにカレンダーの次の予定を表示してくれる。Enter でそのまま Meet や Zoom を開いてくれるのでキーボードから手を離す必要がない。

f:id:motemen:20220128231151p:plain

もともとカレンダーの確認には Dato を使っていたし今も使ってるが、これでミーティングへのアクセスがかなりよくなった。

コミュニティベースの Store で機能を追加できる

https://www.raycast.com/store にいろんな extension がアップロードされているし、この検索やインストールも Raycast 上で行える。Extension ってのは補完候補を増やしたり、Raycast 上から何らかのアクションを起こせるようにするやつだ。

自分の場合以下のような extension をインストールして、いろんな作業の起点を Raycast にできている。

  • Google Workspace
    • Google ドライブのファイルの検索ができる。これめっちゃ助かる。
  • Google Chrome
    • Chrome のタブを操作したり選択したりできるらしい。が複数ウィンドウ開いてるとあまりちゃんと動いてくれないことがある……。
  • TickTick
    • 以前のエントリで紹介した TickTick のタスクもここから確認できて便利。

Extension は TypeScript/React で書ける!!!

これ、なるほど便利だなーとなった。Raycast の追加機能は TypeScript/React で書くことになっている。

以下のサイトに詳しく書かれているのでエッセンスのみ紹介する。

Introduction - Raycast API

  • コマンドは React コンポーネントとして実装する。@raycast/api<List><ActionPanel> などを使うことで、候補アクションや取れるサブアクションをユーザに提示できる。
  • Node のランタイムが提供されるので、fetch() とかが普通に使える。
  • 手元で開発するときは @raycast/api 同梱のコマンドを使って ray develop すると、自動リロードしつつ手元の Raycast に反映してくれる。
  • API キーなど、Extension に設定項目が必要な場合は package.json に書いとくと自動的に UI を提供してくれる。

自分で extension を作ってみる

とまあ非常に楽を追究していて、スムーズに開発できるのがよい。そういうわけで1つ作ってみた。ウェブ API でなにかの一覧を提供してるものがほしかったので、手近な Mackerel のアラートを一覧するようなもの。

GitHub - motemen/raycast-extension-mackerel-alerts

API を利用するのに API キーが必要なので、package.json に書いておく。"required": true とすることで、この extension を使う初回に勝手にダイアログを出してくれるので便利。

  "preferences": [
    {
      "name": "apiKey",
      "type": "password",
      "required": true,
      "title": "API key",
      "description": "Mackerel API key",
      "placeholder": "Visit https://mackerel.io/my?tab=apikeys to obtain one"
    }
  ],

中身は swr と axios でごくごく普通に作る。選択したときのアクションはブラウザを開く、とし、アラートの重大度に合わせて色もつけてみる。

export default function Command() {
  const apiKey = preferences.apiKey?.value as string | undefined;

  const { data, error } = useSWR(apiKey, async (apiKey) => {
    ...
  });

  ...

  return (
    <List isLoading={!data && !error}>
      {data?.alerts.map((alert) => (
        <AlertListItem key={alert.id} orgName={data.orgName} alert={alert} />
      ))}
    </List>
}
const AlertListItem = ({ orgName, alert }: { orgName: string; alert: AlertResponseItem }) => (
  <List.Item
    title={`${alert.type}: ${alert.message || alert.hostId || alert.value}`}
    ...
    // アイテムに対するアクションは actions で設定する
    actions={
      <ActionPanel>
        <OpenInBrowserAction url={`https://mackerel.io/orgs/${orgName}/alerts/${alert.id}`}></OpenInBrowserAction>
      </ActionPanel>
    }
    // なんかいい感じのアイコンが提供されている
    icon={{
      source: Icon.Circle,
      ...
    }}
  ></List.Item>
);

こんな感じに動きます。簡単だった。なんかいいやつ作ったら教えてくれ!

f:id:motemen:20220128231541g:plain

ちなみに同様なもので Command E ってのもあるらしいが、Dropbox に買収されてからなりを潜めていそう。再浮上に期待。ランディングページにインストールへの導線がなくて笑った。

Goで知らないフィールドのあるJSONを取り扱う

野良 HTTP JSON API クライアントを作ってると、API が返してくる JSON の形に確信が持てないし、「これ何に使うんだろ」みたいなフィールドもあったりして struct にエンコードするのをサボったりする。

そういったコードがライブラリとして使われる余地を残すとすると、struct で表現されていないデータにも何らかの方法でアクセスできるようにしておきたい。こういうパターンあるんじゃないかと思うが、みんなどうやってるのか分からなかったのでメモ。

まあ素直に、json.RawMessage を struct に持たせておくのが一番よいだろう。冗長にはなるが、構造体の定義されたフィールドに便利なデータはあるし、より詳細に見たいなら RawMessage 経由で生データを見ればよい。ということにする。また、RawMessage を保持している場合は JSON 化したときにこれをそのまま使いたい。

問題は json.Marhsaler/Unmarshaler の実装だ。まず前提として JSON エンコード・デコードを手書きしたくはない。encoding/json の実装に乗っかりたい。前記の要件を満たすため MarshalJSON/UnmarshalJSON を自作することになるが、そうなると encoding/json の実装をそのまま使うことができない。

そこで型をもう一つ用意することにする。同じ構造体をベースに、json.Marshaler/Unmarshaler を実装しない別の型を作って、これを経由して JSON 化をおこなう。

type Struct struct {
    rawJSON json.RawMessage
}

type _struct Struct

func (s Struct) MarshalJSON() ([]byte, error) {
    if s.rawJSON != nil {
        return json.Marshal(s.rawJSON)
    }
    
    return json.Marshal(_struct(s))
}

func (s *Struct) UnmarshalJSON(data []byte) error {
    err := json.Unmarshal(data, (*_struct)(s))
    if err != nil {
        return err
    }
    
    s.rawJSON = append([]byte{}, data...)
    return nil
}

凝った形状の構造体を作ったり、reflect を使ったりする方法もあるだろうけど、できるだけ素直にやるならこんな感じかなあ。

動く例: https://go.dev/play/p/gdQ4U6W34ef

はてなで一緒に働きませんか?