こんにちは、はっきーです。東京外国語大学の学生で(英検1級・TOEIC 960・IELTS 8.0)、英単語バトルゲーム「WordForge(ワードフォージ)」をひとりで作っています。
アルファベットのタイルで英単語を作って対戦するブラウザゲームで、作った単語にN〜LGの6段階のレアリティがつき、カードとして集まるのが特徴です。この記事では「単語のレアリティをどう決めているか」と「得点の計算」を、実装に沿って書きます。
2026年10月25日時点の仕様です。数値は調整で変わることがあります。
1. レアリティは6段階
| 表示 | 内部キー | 倍率 |
|---|---|---|
| N | normal |
×1 |
| C | common |
×1.5 |
| R | rare |
×2 |
| SR | super_rare |
×3 |
| UR | ultra_rare |
×5 |
| LG | legendary |
×8 |
倍率はそのまま得点にかかります(後述)。「難しい単語を知っている=強い」をゲームのルールにしたかったので、ここがいちばん大事な設計です。
2. レアリティの決め方:3段のフォールバック
1つの単語のレアリティは、次の順番で決めています。
-
活用形の個別表(約4,500語。例:
cats→normal)にあればそれを使う - CEFRレベル表(約4,300語)にあれば、レベルからそのまま決める
- どちらにもなければ、単語の出現頻度(Zipf値) から決める
CEFRレベル → レアリティ
| CEFR | A1 | A2 | B1 | B2 | C1 | C2 |
|---|---|---|---|---|---|---|
| レアリティ | N | C | R | SR | UR | LG |
CEFR表の内訳は A1:600 / A2:800 / B1:707 / B2:721 / C1:784 / C2:706 語です。この表はWordForge用に独自に作ったもので、ゲーム内の英検の級別モード(5級〜1級)もこのCEFRレベルをもとにしています(日本英語検定協会とは関係ありません)。
頻度(Zipf値)→ レアリティ
CEFR表にない単語は、辞書APIが返す頻度(Zipf尺度。だいたい1〜7で、大きいほどよく使われる単語)で決めます。
| Zipf値 | レアリティ |
|---|---|
| 5以上 | N |
| 4以上 | C |
| 2.5以上 | R |
| 1.35以上 | SR |
| 1.12以上 | UR |
| それ未満 | LG |
ただし、
- 頻度が0(またはデータなし)の場合は N
- 3文字以下の単語はSRが上限(短い珍語で一発逆転しすぎないように)
という例外を入れています。
実装(簡略化して書き直したもの)
// 実際のコードを読みやすく書き直したもの(定数は本番と同じ)
type Rarity = "normal" | "common" | "rare" | "super_rare" | "ultra_rare" | "legendary";
const ORDER: Rarity[] = ["normal", "common", "rare", "super_rare", "ultra_rare", "legendary"];
const ZIPF_THRESHOLDS: { rarity: Rarity; min: number }[] = [
{ rarity: "normal", min: 5 },
{ rarity: "common", min: 4 },
{ rarity: "rare", min: 2.5 },
{ rarity: "super_rare", min: 1.35 },
{ rarity: "ultra_rare", min: 1.12 },
{ rarity: "legendary", min: -Infinity },
];
const CEFR_TO_RARITY = { A1: "normal", A2: "common", B1: "rare", B2: "super_rare", C1: "ultra_rare", C2: "legendary" } as const;
const cap = (r: Rarity, max: Rarity) => (ORDER.indexOf(r) > ORDER.indexOf(max) ? max : r);
function rarityFromZipf(zipf: number, length: number): Rarity {
if (!Number.isFinite(zipf) || zipf <= 0) return "normal";
const r = ZIPF_THRESHOLDS.find((t) => zipf >= t.min)!.rarity;
return length <= 3 ? cap(r, "super_rare") : r;
}
function rarityOf(word: string, zipf: number): Rarity {
const w = word.toLowerCase();
if (w in INFLECTION_OVERRIDES) return INFLECTION_OVERRIDES[w]; // 活用形の個別表
if (w in CEFR) return CEFR_TO_RARITY[CEFR[w]]; // CEFRレベル表
return rarityFromZipf(zipf, w.length); // 頻度
}
実際の判定例(本番の辞書APIで確認)
辞書は POST /api/dictionary/lookup に {"word": "...", "direction": "en-ja"} を送ると、和訳・品詞・頻度・レアリティが返ってきます。
{"word":"labyrinth","definitionJa":"迷宮","partOfSpeech":"noun","frequency":3.31,"rarity":"legendary","playable":true}
| 単語 | 和訳 | Zipf | CEFR | レアリティ | 決め手 |
|---|---|---|---|---|---|
| cat | 猫 | 4.78 | A1 | N | CEFR |
| pizza | ピザ | 4.43 | なし | C | 頻度 |
| serendipity | 偶然 | 2.74 | なし | R | 頻度 |
| xylophone | 木琴 | 2.37 | なし | SR | 頻度 |
| ephemeral | はかない | 2.99 | C1 | UR | CEFR |
| aesthetic | 美的な | 4.02 | C2 | LG | CEFR |
| labyrinth | 迷宮 | 3.31 | C2 | LG | CEFR |
おもしろいのは aesthetic です。頻度だけなら4.02でCですが、CEFRがC2なのでLGになります。逆に serendipity は「難しそう」なのにCEFR表になく、頻度2.74でRどまり。「よく見かけるけど学習段階では上級」な単語を拾えるのが、CEFRを優先している理由です。
辞書にない文字列は "playable": false になり、対戦では使えません。
3. 対戦では「7文字以上で1段階アップ」
対戦中は、7文字以上の単語のレアリティが1段階上がります(頻度データがある単語のみ。LGが上限)。たとえば xylophone(9文字)は図鑑ではSRですが、対戦ではURとして計算されます。長い単語を組む手間に報いるためのルールです。
パワーアップの「ゴールドラッシュ」も同じく1段階アップです。
4. タイル:26文字・98枚
| 点数 | 文字(枚数) |
|---|---|
| 1 | A(9) E(12) I(9) O(8) U(4) L(4) N(6) R(6) S(4) T(6) |
| 2 | D(4) G(3) |
| 3 | B(2) C(2) M(2) P(2) |
| 4 | F(2) H(2) V(2) W(2) Y(2) |
| 5 | K(1) |
| 8 | J(1) X(1) |
| 10 | Q(1) Z(1) |
バッグは「文字×枚数」の配列を作り、シード値つきでシャッフルして先頭から引くだけのシンプルな作りです(同じシードなら同じ並びになります)。対戦人数は1〜8人で、3人以上のときはこのセットを2つ入れます。手札は10枚です。ワイルドタイルは0点として数えます。
5. 得点の計算式
基本点 = タイルの点数の合計 + (文字数 - 3) × 2 ※0未満にはしない
長さボーナス = 7文字以上:+8 / 6文字:+5 / 5文字:+2 / それ以下:0
得点 = floor( (基本点 + 長さボーナス) × レアリティ倍率 × コンボ倍率 × その他の倍率 )
コンボ倍率 = 1 + コンボ数 × 0.15
- コンボ:前のターンの自分の単語より長い単語を作ると1つ増え、同じ長さ以下や辞書にない単語で0に戻ります
- その他の倍率:ダブル(×2)、リンク(頭文字が前の単語と同じなら×1.5)、フォートレス(相手の得点を30%カット)など。オーバードライブは長さボーナスを2倍にします
計算例
| 単語 | タイル | (文字数-3)×2 | 長さ | 小計 | レアリティ(対戦) | 得点 |
|---|---|---|---|---|---|---|
| ennui | 5 | 4 | 2 | 11 | LG ×8 | 88 |
| quaff | 20 | 4 | 2 | 26 | LG ×8 | 208 |
| xylophone | 24 | 12 | 8 | 44 | UR ×5(SR→1段階アップ) | 220 |
同じ5文字でも、N(×1)なら11点、LG(×8)なら88点。タイルの点数よりレアリティの倍率のほうがずっと効くようにしてあります。
6. 技術スタック(ざっくり)
- フロントエンド:React、Viteでビルド。ルーティングは wouter、サーバーとの通信は TanStack Query、UIは Radix UI と Tailwind CSS、アニメーションは Framer Motion
- バックエンド:Node.js(Express)。辞書の検索(和訳・頻度・レアリティ)はAPIで返します
- オンライン対戦:WebSocket でルームコードを共有して対戦
- ホスティング:Replit
7. 作ってみてわかったこと
- 頻度だけだと「教科書的な難しさ」とずれる:aesthetic のように日常でよく見る上級語が低レアになってしまうので、CEFRを優先する形に落ち着きました
-
活用形は別扱いが必要:
catsのような活用形をそのまま頻度で判定すると、元の単語とレアリティがずれることがあるので、約4,500語の個別表で調整しています - 短い珍語にはふたを:3文字以下はSRまで。これがないと短い単語が強すぎました
遊んでみて「この単語がこのレアリティなのはおかしい」というのがあれば、ぜひ教えてください。
👉 WordForge(無料・登録不要・スマホ/PC):https://word-forge.replit.app/?utm_source=qiita&utm_medium=article&utm_campaign=devlog01
👉 12月5日(土)〜6日(日)に第1回オンライン英単語大会「#WordForge杯」(参加無料):https://connpass.com/event/410982/
最後に少しだけ宣伝です。教室・学校への無料提供を広げるため、CAMPFIREでクラウドファンディングを準備しています(現在審査中)。始まったときにお知らせが届く事前登録はこちらです(メールアドレスだけ・1分):https://forms.gle/B6A4wWWZSF9gvPmY6
はっきー(WordForge 開発者)/X:@WordForgeJP