0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

タイプ相性表を「等倍を省略したスパース表」で持つ:複合タイプの掛け算・0倍の伝播・チェッカーUI(JavaScript)

0
Posted at

筆者は攻略サイト『ポケチャン攻略ラボ』の運営者です。 この記事では、サイトのタイプ相性チェッカーとダメージ計算で共有している「タイプ相性表」の実装を解説します。コードはリポジトリから要点だけを抜き出して短くしたものです。
※ポケチャン攻略ラボは非公式のファンサイトです。任天堂・クリーチャーズ・ゲームフリーク・株式会社ポケモンとは関係ありません。

はじめに

ポケモンのタイプは 18 種類で、攻撃側の技タイプ × 防御側のタイプごとに倍率が決まります。単タイプなら表を 1 回引けば足りますが、複合タイプでは 2 つのタイプへの倍率を掛け合わせる必要があります。さらに攻略サイトでは、同じ表を次の場所から同時に使います。

  1. タイプ相性チェッカー(防御側・攻撃側の一覧)
  2. タイプ相性表のマトリクス表示
  3. ダメージ計算・対面シミュレーター(typeMult として最終ダメージに載せる)

ここでやりがちな失敗は、「ページごとに相性表をコピペする」「等倍も含めた 18×18=324 マスを全部書く」「複合タイプの掛け算を UI 側だけでやって計算側と食い違う」ことです。このサイトでは、相性表を 1 つの JSON に集約し、等倍(1)は省略したスパース表にしています。テストで件数と代表例を固定しているので、表をいじるとすぐ落ちます。

この記事で扱うのは次の 5 点です。

  1. データ形状(packages/data/types.json)
  2. ルックアップ関数 eff / effMulti
  3. チェッカー UI(TypeChecker.astro)とマトリクス(TypeChart.astro)
  4. ダメージ計算への載せ方(typeMult と切り捨て)
  5. テストでロックしている数字

※テラスタルや「ふゆう」などの特性による無効は、この相性表そのものには含まれません。後述します。

1. 唯一のデータソース:types.json

サイト側の薄いラッパはこうなっています。

// website/src/data/types.mjs(抜粋)
// タイプ・相性表は packages/data/types.json が唯一のデータソース
import data from '@pokechamp/data/types.json' with { type: 'json' };

export const TYPES = data.types.map((t) => ({
  id: t.id, ja: t.nameJa, short: t.short, color: t.color, fg: textColorOn(t.color),
}));
export const CHART = data.chart;
export const TYPE_BY_ID = Object.fromEntries(TYPES.map((t) => [t.id, t]));

実体は @pokechamp/data(現在 0.8.7)の types.json です。Web サイトもアプリも同じ JSON を読みます。meta には更新日と照合ソースが入ります。

// packages/data/types.json(meta と types の先頭だけ)
{
  "meta": {
    "updatedAt": "2026-10-18",
    "note": "chart は攻撃側→防御側の倍率。等倍(1)は省略。本編第6世代以降と同一。",
    "sources": [
      { "name": "タイプ相性とタイプバランスチェッカー(ゲームエイト)", "url": "…" },
      { "name": "タイプ相性表(PChamp DB)", "url": "…" },
      { "name": "ポケモンタイプ相性表(ポケモン徹底攻略)", "url": "…" }
    ]
  },
  "types": [
    { "id": "normal", "nameJa": "ノーマル", "short": "ノ", "color": "#9fa19f" },
    { "id": "fire",   "nameJa": "ほのお",   "short": "炎", "color": "#e42828" }
    // …合計 18 タイプ
  ]
}

JSON Schema(packages/data/schema/types.schema.json)では、chart の値は 0 / 0.5 / 2 だけを許可しています。等倍の 1 を書かせないことで、「省略ルールを破って等倍を埋める」ミスをスキーマ側でも防いでいます。

2. スパース表:等倍はキーごと無い

chart は「攻撃側 ID → 防御側 ID → 倍率」の二重オブジェクトです。等倍はキーが無い状態です。

// packages/data/types.json(chart の一部)
"dragon": { "dragon": 2, "steel": 0.5, "fairy": 0 },
"electric": {
  "water": 2, "electric": 0.5, "grass": 0.5,
  "ground": 0, "flying": 2, "dragon": 0.5
},
"ice": {
  "fire": 0.5, "water": 0.5, "grass": 2, "ice": 0.5,
  "ground": 2, "flying": 2, "dragon": 2, "steel": 0.5
}

件数を数えると次のようになります(テストでも同じ数字を断言しています)。

項目 値
タイプ数 18
理論上のマス 18×18 = 324
明示エントリ(非等倍) 120
省略された等倍 204
うち 2 倍 51
うち 0.5 倍 61
うち 0 倍 8

0 倍は次の 8 組だけです。

攻撃 防御
ノーマル ゴースト
かくとう ゴースト
ゴースト ノーマル
でんき じめん
じめん ひこう
エスパー あく
どく はがね
ドラゴン フェアリー

スパースにした理由は単純で、書く量が約 1/3 になり、差分レビューで「本当に変わったマス」だけが見えるからです。代わりに「キーが無い=1」という約束をルックアップ側で守る必要があります。

3. eff と effMulti:掛け算で複合タイプを扱う

// website/src/data/types.mjs
export function eff(atk, def) {
  const v = CHART[atk]?.[def];
  return v === undefined ? 1 : v;
}
/** 防御側(単/複合)に対する倍率 */
export function effMulti(atk, defs) {
  return defs.filter(Boolean).reduce((m, d) => m * eff(atk, d), 1);
}

ポイントは 3 つです。

  1. 未定義は 1(スパース表の約束)
  2. defs を filter(Boolean) してから掛ける(単タイプで空文字や null が混ざっても無視)
  3. 掛け算なので、どちらかが 0 なら全体が 0(2×0=0、0.5×0=0)

代表例(テストと同じ):

eff('dragon', 'fairy')                       // → 0
effMulti('ice', ['dragon', 'ground'])        // → 2 × 2 = 4
effMulti('fire', ['water', 'rock'])          // → 0.5 × 0.5 = 0.25
effMulti('ground', ['electric', 'flying'])   // → 2 × 0 = 0

対面シミュレーター側にも同じロジックの小さな工場関数があります。チャートを引数で受け取り、同じ掛け算を返します。

// website/src/lib/battle-sim.mjs
export function makeEff(chart) {
  const eff = (atk, def) => chart[atk]?.[def] ?? 1;
  return (atk, defs) => defs.filter(Boolean).reduce((m, d) => m * eff(atk, d), 1);
}

?? 1 と === undefined ? 1 は、この表では値が 0 になり得るので || 1 にしてはいけない、という点で同じ意図です。0 は falsy なので || だと無効が等倍に化けます。

4. チェッカー UI:倍率ごとにタイプを仕分ける

ページ website/src/pages/tools/type-checker.astro はシェルで、本体は TypeChecker.astro です。クライアント側で effMulti / eff を呼び、結果を 6 段に分けます。

// website/src/components/TypeChecker.astro(script 抜粋)
const GROUPS = [
  [4, '4倍(ちょうバツグン)'],
  [2, '2倍(バツグン)'],
  [1, '等倍'],
  [0.5, '0.5倍(いまひとつ)'],
  [0.25, '0.25倍(かなりいまひとつ)'],
  [0, '0倍(効果なし)'],
];

function renderDef() {
  const t1 = $('tc-t1').value;
  let t2 = $('tc-t2').value;
  if (t2 === t1) t2 = '';                 // 同じタイプを選んだら単タイプ扱い
  const defs = [t1, t2].filter(Boolean);
  const rows = GROUPS.map(([m, label]) => {
    const hits = TYPES.filter((a) => effMulti(a.id, defs) === m);
    return hits.length
      ? `<tr><th>${label}</th><td>…バッジ…</td></tr>`
      : '';                               // 該当ゼロなら行ごと出さない
  }).join('');
  // …
}

初期選択は「タイプ1=ドラゴン(index 14)」「タイプ2=じめん(index 8)」「攻撃側=こおり(index 5)」です。この組み合わせだと effMulti('ice', ['dragon','ground']) === 4 になり、画面を開いた瞬間に 4 倍の例が見えます。

攻撃側モードは単タイプ相手だけを対象に、2 / 0.5 / 0 の 3 段で仕分けます(等倍は出さない)。防御側モードだけが 4 / 0.25 を含むのは、複合タイプの掛け算結果としてだけ現れる倍率だからです。

マトリクス表示(TypeChart.astro)は 18×18 を静的に描き、記号は ◎=2 / △=0.5 / ×=0 / 空欄=等倍です。ここでも eff(a.id, d.id) を使うので、チェッカーと数字がずれません。

バッジの文字色(WCAG)

タイプ色の上に白文字を置くと読めない色があります。types.mjs では相対輝度からコントラスト比を計算し、白で 4.5:1 を満たす(または白の方が #1a1a1a より良い)なら白、そうでなければほぼ黒にしています。

// website/src/data/types.mjs(抜粋)
export const textColorOn = (bg) =>
  (ratio(bg, '#ffffff') >= 4.5 || ratio(bg, '#ffffff') >= ratio(bg, '#1a1a1a')
    ? '#fff' : '#1a1a1a');

実データだと、例えばノーマル(#9fa19f)やでんき(#e0b000)は暗い字、ほのお(#e42828)やゴースト(#704170)は白字になります。TypeBadge.astro も同じ fg を使います。

5. ダメージ計算では typeMult を切り捨てで載せる

相性表の出力は、ダメージ式では typeMult という 1 つの倍率になります。取り得る値のコメントは次のとおりです。

// website/src/lib/damage.mjs(JSDoc 抜粋)
// @param {number} [p.typeMult] タイプ相性倍率 (0,0.25,0.5,1,2,4)

適用順は「乱数 → STAB → タイプ相性 → やけど → 壁など」で、相性は Math.floor です。

// website/src/lib/damage.mjs(抜粋)
if (typeMult === 0 || p.power <= 0) return Array(16).fill(0);
// …
for (let r = 85; r <= 100; r++) {
  let d = Math.floor((base * r) / 100);
  if (stab) d = pokeRound(d, stabMod);
  d = Math.floor(d * typeMult);   // タイプ相性:倍率ごとに切り捨て
  // …
}

typeMult === 0 のときは 16 通りすべて 0 を返して早期終了します。computeRolls(calc-core.mjs)では、まず effMulti(引数名は a.eff)で相性を取り、その後に特性・持ち物の無効(fx.immune)や「ゴースト無視」(ignoreGhost)で上書きします。

// website/src/lib/calc-core.mjs(抜粋)
let typeMult = fx.ignoreGhost
  ? a.eff(fx.moveType, defTypes.filter((x) => x !== 'ghost'))
  : a.eff(fx.moveType, defTypes);
if (fx.immune) typeMult = 0;

つまり 相性表は純粋なタイプ×タイプで、特性・持ち物・フィールドによる「相性以外の無効」は別レイヤです。タイプチェッカーの説明文にも「特性(ふゆう など)や持ち物による無効化・半減はこの表には含まれません」と書いてあります。

6. テストでロックしていること

website/tests/stats.test.mjs の「タイプ相性の代表例」が、表の件数と掛け算の代表値を固定しています。

// website/tests/stats.test.mjs
test('タイプ相性の代表例', () => {
  assert.equal(TYPES.length, 18);
  assert.equal(Object.keys(CHART).length, 18);
  assert.equal(eff('dragon', 'fairy'), 0);
  assert.equal(effMulti('ice', ['dragon', 'ground']), 4);
  assert.equal(effMulti('fire', ['water', 'rock']), 0.25);
  assert.equal(effMulti('ground', ['electric', 'flying']), 0);
  let zeros = 0;
  for (const a in CHART) for (const d in CHART[a]) if (CHART[a][d] === 0) zeros++;
  assert.equal(zeros, 8);
  let two = 0, half = 0;
  for (const a in CHART) for (const d in CHART[a]) {
    if (CHART[a][d] === 2) two++;
    if (CHART[a][d] === 0.5) half++;
  }
  assert.equal(two, 51);
  assert.equal(half, 61);
});

ここで落ちる典型例は次のようなものです。

  • 新しい世代の変更を入れて 0 倍が 9 組になった(または 7 組に減った)
  • 等倍を誤って 1 で埋めて Object.keys(CHART[a]) の数え方が変わった(このテストは 0/2/0.5 の個数なので、等倍を足しても zeros/two/half は同じだが、スキーマが 1 を拒否する)
  • eff を || 1 に変えて、ドラゴン→フェアリーが 1 になる

ダメージ計算の結合テスト(exact.test.mjs や multiacc.test.mjs)も effMulti を computeRolls に渡しているので、表が壊れれば乱数 16 通りの期待値も連鎖して壊れます。

7. この実装に含めていないもの

読者の疑問になりやすいので、コード上の境界をはっきり書いておきます。

話題 この相性表・チェッカー 別レイヤ
複合タイプの 4 / 0.25 / 0 ✅ effMulti —
特性「ふゆう」「もらいび」などの無効 ❌ calc-effects.mjs の immunity
ノーマル技をフェアリー化する(ピクシーレート等) ❌ typeChange(技タイプを変えてから相性を引く)
テラスタル ❌(リポジトリにテラスタル用の相性分岐は無し) —
サイコフィールドの先制無効 ❌ calc-core.mjs が typeMult を後から 0 にする

チェッカーは「タイプだけ」を見る道具、ダメージ計算は「タイプ+特性+持ち物+フィールド」を重ねる道具、という分担です。

まとめ

  • 相性表は @pokechamp/data の types.json が唯一のソース。等倍は省略し、明示エントリは 120 件(2×51、0.5×61、0×8)
  • eff は未定義を 1、effMulti は掛け算。0 は乗法で伝播する。|| 1 は使わない
  • チェッカー UI は防御側を 6 段(4 / 2 / 1 / 0.5 / 0.25 / 0)、攻撃側は単タイプ相手の 3 段
  • ダメージ計算では typeMult を Math.floor で載せ、0 なら 16 通りすべて 0
  • stats.test.mjs がタイプ数・0 倍 8 組・2 倍 51・0.5 倍 61・複合の代表例をロックしている

タイプ相性チェッカーは次のページで試せます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?