尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗小学 2 年生が、学校で九九が始まる前に全部言える状態になることを目的とした PWA。
報酬をアプリの中に置かない。教室で「これ知ってる」となる体験を報酬とする。
設計の方針と判断の根拠は docs/design-notes.md を参照。
ストーリーモードの設計と判断の根拠は docs/story-mode-design.md。
ランクと見た目の判断は docs/rank-design.md。
子どもが話を注文する仕組みの判断は docs/story-authoring-design.md。
話の追加手順は docs/story-prompt.md(親が LLM に投げる雛形)。
| 項目 | 状態 |
|---|---|
| モードの切り替え(たたかう / ぼうけん) | 実装済み |
| 九九のタイムアタック(10 問) | 実装済み |
| 段ごとの解放(順序は設定で変更可能) | 実装済み |
| 問題の音声読み上げ | 実装済み |
| 親子で同じ問題セット。親の記録が常に表示 | 実装済み |
| 段ごとの遅さ・誤答の可視化(親のみ) | 実装済み |
| 問題文・世界観の差し替え | 実装済み(JSON 4 テーマ) |
| 親子で端末が分かれた場合の記録・設定の同期 | 実装済み・実プロジェクトで動作確認ずみ |
| ストーリーモード(街・散策・道の選択・戦い) | 実装済み(2 エリア同梱・各 720 通り) |
| タイムに応じたランク(段ごとに目標が違う) | 実装済み |
| 習熟度による段の解放 | 未実装(設計のみ) |
| 子どもが話を注文して作ってもらう仕組み | 実装済み(要 Supabase。生成は親の Claude) |
| プッシュ通知 | 未実装 |
npm install
npm run dev # http://localhost:3000
npm run build # 本番ビルド
npm run typecheck # 型チェック
npm test # 単体テスト
npm run layout-check # ホームが端末の高さに収まっているかを 6 サイズで確認
npm run stories # 注文された話を作る道具(手順は docs/story-task.md)
npm run icons # アイコン PNG の再生成
ブラウザのブックマークでは開かれない。ホーム画面に置くこと。
親の端末で、デプロイ先の URL から親画面を開き、「べつの端末とつなぐ」の 「子どもの端末で開く QR を出す」を押す。子どもの端末のカメラで読み取ると、アプリが開く。
# 以降)に入れており、サーバへは送られない。
読み取った側では、読み込み直後にアドレスバーから消すアイコンと名称は意図的にゲーム側へ振っている。 「勉強」「ドリル」「学習」という語は画面のどこにも出していない。
src/
app/
page.tsx ホーム。相手の選択と、親の記録の常時表示
play/ タイムアタック本体
story/ ストーリーモード
parent/ 親専用画面(段の解放、弱点、着せ替え)
join/ 親画面の QR から開く入口。合言葉を受け取る
manifest.ts PWA マニフェスト
components/
PlayScreen.tsx 出題・判定・計測
Result.tsx 結果。答えをタップで聞き返せる
StoryScreen.tsx 街・散策・道の選択の進行
StoryBattle.tsx ストーリー中の戦い
StageInsights.tsx 子どもの状態(親のみ)
RankBadge.tsx ランクの表示
ParentGate.tsx 親画面の入口
lib/
kuku.ts 出題の組み立てと重み付け、入力判定
rank.ts 段ごとのランクのしきい値と判定
yomi.ts 九九の唱え方テーブル
speech.ts Web Speech API のラッパ
themes.ts 着せ替えの読み込み
story.ts ストーリーの型と検証
stories.ts 散策の組み立てと読み込み
storage/ 保存層(local / supabase)
data/themes/*.json テーマ定義
data/stories/*.json ストーリー
実装を変更する際は以下を壊さないこと。いずれも設計メモ上の判断に対応する。
lib/kuku.ts の weightOf)にのみ反映する。.app[data-fit] と .scrollArea)。角を斜めに落とした HUD(アーケード筐体の読み方)で統一している。
判断の根拠は docs/rank-design.md の判断 7。
clip-path による切り欠き)・発光・地の質感・量の刻み方で出すarcade(シアン)だが、緑・紫・青でも成立する触るときに踏みやすい点が 3 つある。
clip-path は box-shadow を切り落とす。 角を落とした要素の外側の光は
filter: drop-shadow() で出すことbody::before / body::after)は z-index: 0、中身は 1。
body に不透明な背景を戻すと地の格子が隠れる(描画順の問題).panel の切り欠きは 14px ある。
答えの札のような小さい箱には .factCell のように専用の値を持たせるタイムでゴールド・シルバー・ブロンズが付く。判断の根拠は docs/rank-design.md。
src/lib/rank.ts)ブロンズの境目が 1 分を超えないことは単体テストで固定してある。 「1 回 1 分で終わる長さ」をランクの境目が壊さないため。
ホームは外枠を画面の高さに固定し(.app[data-fit='true'])、
あふれる可能性があるのは中身(.scrollArea)だけにしてある。
開いて最初に押す「はじめる」が画面の外に出ないことを構造で保証するため。
画面の高さに応じた圧縮を 3 段(790px / 620px / 580px 以下)置いている。 レイアウトを変えたら 1 つの画面サイズで確かめて終わりにしないこと。 320×568 から 430×932 までで、次が崩れていないことを確認する。
目視では分からないので npm run layout-check で測る。
npm run build && npm start -- -p 3210 &
npm run layout-check
崩れていたら終了コード 1 と、どのサイズでどの条件が落ちたかを出す。
Chromium の場所が違う場合は CHROMIUM_PATH で指定する。
このアプリで刺さったのは「話を読むこと」ではなく「話を作らせること」だった。 実機で確認したところ、同梱の話は使われていなかった。元々 AI に話を作らせて遊んでいた子なので、 楽しんでいたのは注文する側であって、出来上がったものを読む側ではなかった。
設計メモは最初からそう書いていた。
報酬は「話の続きが読めること」ではなく「自分の考えた世界が動いていること」。
判断の根拠は docs/story-authoring-design.md。
子どもが注文する → Supabase に残る → 親の Claude が作る →「ぼうけん」に並ぶ
(アプリ) (status: ordered) (スケジュールタスク) (status: ready)
LLM を API として呼ばない。 話を書くのは、スケジュールタスクを実行している Claude 自身である。Anthropic の API は従量課金で、claude.ai のサブスクリプションとは 別請求になる。子どもが注文するたびに課金される構造は、家庭用のアプリには重い。
アプリ側は注文を置くだけで、生成には一切関わらない。生成を叩ける URL も存在しない。
publish が検査に通らなければ書き込まない。
読む側(アプリ)も読み込んだ話を毎回検査し、通らないものは遊べる一覧に出さない。
Supabase は手でも触れる以上、壊れた話が子どもの画面に出る経路を残さないdocs/story-prompt.md から実行時に読む。 親が手で LLM に投げるときと
同じ文面を使う。二重に持つと必ず片方が古くなるfailed として残し、
子どもの画面に「うまく できなかったみたい」と出す。
注文が消えたように見えると、次を注文しなくなるsupabase/schema.sql を実行済みであること(まだなら先に実行する)supabase/schema-stories.sql を SQL エディタで実行する.env.local に FAMILY_CODE(親画面で設定した合言葉)を足すdocs/story-task.md の「タスクに渡す文面」で Routine を作る。
親が手で動かしたいときも、同じ文面を投げればよい手順の詳細と、書き手が守ることは docs/story-task.md。
ホーム上部で「たたかう」と「ぼうけん」を切り替える。画面遷移ではなく中身の入れ替えにしてあるため、 「はじめる」までのタップ数は増えない。選んだ側は端末に覚えさせ、次に開いたとき同じ側から始まる。
動機の構造が違うため、2 つを統合しない。
| 動機の源 | |
|---|---|
| たたかう | 親の記録を抜くこと |
| ぼうけん | 自分の作った世界が動いていること |
片方が刺さらなくても、もう片方が残る。
骨組みは固定で、中身だけが毎回引き直される。
街 → 道をえらぶ → 場面 → 出会い(戦い) → 場面 → 出会い(戦い) → 街へ帰る
骨組みを固定するのは 1 ループの長さを保つため。ここが可変だと、日によって 1 回の長さが変わる。 中身を引くのは組み合わせで増やすため。場面を 1 つ足すだけで通り数が増える。
直近に出たものは次の散策で避ける。組み合わせがあっても、同じ相手が続けて出ると「また同じ」に感じられる。
問題は平読み(「ろく かける なな」)、答えは九九の唱え方(「ろくしちしじゅうに」)で読み上げる。
唱え方は覚える対象そのものなので、誤答時と結果画面のタップ時にのみ再生する。
読みのテーブルは src/lib/yomi.ts にある。
iOS Safari では最初の発話がユーザー操作起点である必要があるため、
「はじめる」のタップ時に primeSpeech() を呼んでいる。
src/data/themes/ に JSON を置き、src/lib/themes.ts の THEMES に追加する。
外装は共通なので、足すのは色と文言と敵だけでよい。
問題文の着せ替えは LLM で生成してよいが、生成結果は JSON として保存し、実行時には呼ばない。
応答速度と費用の両面で、毎回呼ぶ形にはしないこと。
敵の名前は親画面から編集でき、その場で反映される。 子どもに決めてもらうための項目であり、この用途を想定して即時反映にしている。
Supabase を有効にすると、記録と共有設定が端末間で同期される。 設定しない場合は端末内(localStorage)で完結し、親は子の端末の親画面から記録を作る。
Supabase プロジェクトを作り、supabase/schema.sql を SQL エディタで実行する
(中身はすべて冪等なので、更新があれば何度流し直しても安全)
環境変数を設定する
cp .env.example .env.local
# NEXT_PUBLIC_SUPABASE_URL と NEXT_PUBLIC_SUPABASE_ANON_KEY を設定
親子それぞれの端末で、親画面の「べつの端末とつなぐ」に同じ合言葉を入れる
親の端末では「この端末は」を「おうちのひとの端末」に切り替える
| 種類 | 例 | 同期 |
|---|---|---|
| 記録 | タイム、正答数、問題ごとの結果 | する |
| 共有設定 | 段の解放、テーマ、敵の名前、呼び名 | する |
| 端末設定 | 合言葉、この端末の持ち主、音声の ON/OFF | しない |
| ストーリー | 場面、あいて、セリフ | しない(静的 JSON) |
合言葉と持ち主を同期させると意味が壊れる。 親の端末の設定が子の端末へ流れてきて、子どもの記録が親のものとして残る事故になる。
update 権限を渡さないx-family-code で行う。合言葉は推測されにくい長い文字列にすること同期が成功することと、他人から守られていることは別である。 RLS が効いていなくても、正しい合言葉での同期は成功してしまう。
片方の端末の合言葉をでたらめな文字列に変えて、
相手の記録が見えなくなることを確認すること。
見えたままなら RLS が効いていない。supabase/schema.sql のポリシーを流し直す。
合言葉は実質これだけが鍵になる。anon キーは公開されるものなので、 短い合言葉にすると誰でも読み書きできる状態に近づく。
Supabase のダッシュボード(Advisors)か MCP の get_advisors で、RLS の抜けや
関数の設定を機械的に点検できる。実際、request_family_code の search_path が
固定されていない指摘が出たため schema.sql で set search_path = '' を付けた。
この関数はすべての RLS ポリシーから呼ばれるので、ここが緩いと分離の根が緩む。
| 問い | 見るもの | 実装 |
|---|---|---|
| 開かれているか | 親が言わずに開いた回数 | 親画面「この 7 日で開いた回数」 |
| 覚えたか | 学校で九九が始まったときに既に言えたか | アプリ外で確認する |
| 態度が変わったか | 九九以外の勉強に対する反応 | アプリ外で確認する |
連続日数やプレイ時間は成功指標にしない。 増やそうとすると子どもの時間を奪う方向に働き、目的と逆行する。