AI Dev Lab
RuneCast

RuneCast ができるまで — Elder Futhark 24 文字を引く

北欧ルーン占いの開発ログ。 Elder Futhark 24 文字を 3 つのアエッティルに分け、 対称形のルーンは逆位置を持たないという伝統を実装に落とし込むまでと、 3 種類のスプレッドの作り方を記録しています。

RuneCast は、 北欧 の ルーン文字 (Elder Futhark 24 文字) を 投げて 引く 占い。 石 を 転がす 演出 の あと、 正位置 / 逆位置 の 意味 と 助言 が 出ます。 引く 処理 は すべて ブラウザ の 中 で、 質問 文 も サーバー に 送られ ません。

#🎯 タロット と 同じ 作り に しない

占い 系 は これ が 何本目 か という 状態 で、 すでに 数秘術 ・ タロット ・ マヤ暦 ・ 十干十二支 ・ 西洋占星術 が 並んで いました。 一番 近い の は タロット (ArcanaFlip) です が、 同じ 「カード を めくる」 体験 を もう 一度 作って も 意味 が ありません。

ルーン に は タロット に ない 前提 が 2 つ あります。

  1. 文字 数 が 24 と 少なく、 1 文字 ずつ に 名前 と 意味 が ある
  2. 文字 の 形 が 左右対称 な もの は、 逆位置 が 存在 しない

この 2 つ目 が 決め手 でした。 78 枚 すべて に 正逆 が ある タロット と 違い、 ルーン は 形 の 都合 で 逆位置 を 持て ない もの が ある。 これ を 実装 に 落とし込む と、 それ だけ で 別 の ツール に なり ます。

#🏗️ 「逆位置 を 持て ない ルーン」 を データ に 持たせる

各 ルーン に reversible を 持たせ ました。

ts
export interface Rune {
  id: string
  glyph: string        // ᚠ ᚢ ᚦ …
  name: string         // 翻字 (Fehu / Uruz …)
  jp: string           // フェフ・家畜
  keyword: string      // 富 / 豊かさ
  upright: string
  reversed: string
  reversible: boolean  // 対称形 なら false
  aett: 1 | 2 | 3      // 3 つ の アエッティル
}

reversible: false に なる の は、 ハガラズ (ᚺ 雹) ・ イサ (ᛁ 氷) ・ ヤラ (ᛃ 収穫) ・ エイワズ (ᛇ イチイ) ・ ソウィロ (ᛊ 太陽) ・ ギューフ (ᚷ 贈り物) ・ イングワズ (ᛜ) ・ ダガズ (ᛞ 夜明け) — いずれ も 上下 を ひっくり返して も 同じ 形 に なる 文字 です。

引く 処理 は こう なり ます。

ts
export function castRunes(spread: Spread): DrawnRune[] {
  const pool = [...RUNES]
  const out: DrawnRune[] = []
  for (let i = 0; i < spread.count; i++) {
    const idx = Math.floor(Math.random() * pool.length)
    const rune = pool.splice(idx, 1)[0]       // 重複 なし
    const reversed = rune.reversible && Math.random() < 0.5
    out.push({ rune, reversed, position: spread.positions[i] })
  }
  return out
}

rune.reversible && の 一語 だけ で、 「対称 な ルーン は 何度 引いて も 必ず 正位置」 が 成立 します。 逆位置 を 持た ない ルーン に は、 「逆位置 なし」 と 明記 した うえ で 停滞 側 の 読み を 添え ました。 ハガラズ の ように もともと 試練 を 意味 する 文字 は、 正位置 の まま で 十分 厳しい ので、 無理 に 逆 を 作ら ない 方 が 伝統 に 沿い ます。

#🔑 24 文字 を 3 つ の アエッティル に 割る

Elder Futhark は 8 文字 ずつ 3 群 (アエッティル) に 分かれ ます。 第 1 が フレイヤ、 第 2 が ヘイムダル、 第 3 が テュール。 aett: 1 | 2 | 3 を 持たせて 一覧 を 区切る と、 24 個 の 羅列 が 意味 の ある 3 ブロック に 見え ます。

スプレッド は 3 種類 に 絞り ました。

スプレッド枚数位置
一筋1今 の 導き
運命の三女神3ウルド (過去) / ヴェルザンディ (現在) / スクルド (未来)
五連の石5状況 / 障害 / 助言 / 基盤 / 結果

三女神 の 名前 は 北欧神話 の ノルン から 取って います。 「過去 ・ 現在 ・ 未来」 と だけ 書く より、 ウルド / ヴェルザンディ / スクルド と 併記 した 方 が 世界観 が 立ち、 検索 でも 拾わ れ ます。

#😓 苦労した ところ

引く 前 の 間 (ま)。 ボタン を 押した 瞬間 に 結果 が 出る と、 占い に なりません。 石 が 転がる 演出 を 挟んで 850ms 待ち、 その 間 は 結果 を null に 戻して います。 短すぎる と 軽く、 長すぎる と ただ の 待ち時間 に なる ので、 数字 を 何度 か 振り直し ました。

逆位置 の 表示。 逆位置 の ルーン は CSS の rotate(180deg) で 実際 に ひっくり返して います。 文字 が 上下 逆 に 見える こと 自体 が 説明 に なる ので、 ラベル より 先 に 形 で 伝わり ます。

ルーン文字 の フォント。 ᚠ ᚢ ᚦ … は 環境 に よって 豆腐 (□) に なる 可能性 が あり ます。 実機 で 確認 し、 グリフ が 出ない 場合 に 備えて 翻字 (Fehu) と 日本語 通称 (フェフ) を 必ず 併記 する 構成 に しました。

#🔭 今後 の 拡張

  • 質問文 から 決定的 に 引く モード (同じ 問い なら 同じ 結果 に なる ハッシュ 方式)
  • ルーン 1 文字 ずつ の 解説 ページ (24 文字 × 語源 ・ 神話 ・ 現代 の 読み)
  • 引いた 履歴 を 端末 内 に 保存 して 振り返る
  • 石 を 布 の 上 に 投げて 落ちた 位置 で 読む 「キャスティング」 方式

#💡 この サービス から 言える こと

題材 に 固有 の 制約 が ある なら、 それ を 実装 に 落とす と 差別化 に なり ます。 「対称 な 文字 は 逆位置 を 持た ない」 は、 知って いれば 一行 の 条件式 です が、 知ら ずに 作る と ただ の 24 枚 カード に なり ます。

占い 系 は 見た目 だけ 変えて 量産 でき て しまう 領域 です。 だ から こそ、 その 文化 の 中 で 何 が 決まって いる の か を 先 に 調べる 方 が、 結果的 に 早く 別物 に なり ました。

[ ./next_action ]

読んだら、 RuneCast を実際に動かす。

この開発ログは RuneCast をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →