なぜ要るか
ここまでのゲームは、札が5枚・的が3つで固定です。増やそうとすると、コードのあちこちを直すことになります。
var CARDS = [1, 2, 3, 4, 5];
// 的は画面のHTMLに3つ直接書いてある
この形だと、的を1つ増やすだけで直す場所が5つも6つも出てきます。画面、盤面の初期化、勝敗の集計、相手の思考。 どれか1つ忘れると、静かに壊れます。
この術でやるのは1つです。ゲームの形を「データ」にする。 そうすると、内容を足すのがコードを書く作業ではなく、表に行を足す作業になります。
使う道具: なし
AIプロンプト
この術の作業をAIへ任せるときは、これをそのまま貼ってください。
index.html の的を、HTMLに直接書くのをやめて id と name を持つ TARGETS の配列にして。
画面の描画、盤面の初期化、勝敗の集計、相手の合法手の生成を、全部その配列から作って。
的の数を表す 3 という数字が1箇所も残らないようにして。
札のほうも id・label・power を持つ形にして、山の枚数は数字1つから作って。
的や札の部分は、自分のゲームで増えるものと、その個数に置き換えてください。
何をデータにするかを見極める
やみくもに全部データにするのではありません。「増えるかもしれないもの」だけです。
このゲームで増えそうなものは2つです。
| データ | 何が増えるか |
|---|---|
TARGETS |
的の数。3つを4つにしたくなる |
DECK |
札の種類。1〜5を1〜7にしたくなる |
逆に「1手番に1枚置く」「合計で比べる」は、増えるものではなくゲームの芯です。これはコードに書いたままで構いません。増えないものをデータにすると、読みにくくなるだけです。
的を表にします
配列というのは、同じ形のデータを順番に並べておく入れ物です。ここでは的1つを1行として並べます。表計算ソフトの1行1件をイメージしてください。
var TARGETS = [
{ id: 't1', name: '一の的', kesiki: '門の的', mon: 'mon' },
{ id: 't2', name: '二の的', kesiki: '蔵の的', mon: 'kura' },
{ id: 't3', name: '三の的', kesiki: '井の的', mon: 'ido' },
];
これだけです。kesiki と mon は5-5で付けた表示名と紋の指定で、行が増えたのではなく、1行に列が増えただけです。そして画面も盤面も思考も、全部この配列を見るように変えます。
// 盤面の初期化
TARGETS.forEach(function (t) { board[t.id] = { mine: [], theirs: [] }; });
// 画面
el('targets').innerHTML = TARGETS.map(function (t) { ... }).join('');
// 相手が打てる手の一覧(3-4で作ったもの)
hand.forEach(function (cardId) {
TARGETS.forEach(function (t) { moves.push({ card: cardId, target: t.id }); });
});
forEach と map は、配列の行を1つずつ取り出して同じ処理を繰り返す書き方です。つまり「的の行があるだけ繰り返す」ので、行が増えればそのまま増えた分だけ動きます。
3箇所とも TARGETS を見ています。 どこにも 3 という数字がありません。
効果を確かめます
TARGETS に1行足してみました。
{ id: 't4', name: '四の的' },
結果です。
的の数 3 → 4
画面の的 3つ → 4つ(名前も出る)
合法手の数 9通り → 12通り
JSエラー なし
直したコード 0行
合法手というのは、そのときルール上打てる手の一覧です。「この札をこの的に置く」という組み合わせを全部並べたものだと思ってください。
相手の思考まで勝手に付いてきます。 legalMoves() が TARGETS を見ているからです。「札の数 × 的の数」の掛け算を、どこにも書いていません。
これが「データで持つ」の意味です。ルールが増えても、コードは増えない。
札も表にします
札のほうは、もう1段先まで行けます。
var DECK = [];
for (var d = 1; d <= DECK_SIZE; d++) DECK.push({ id: 'c' + d, label: String(d), power: d });
この1行は「1から DECK_SIZE まで数えながら、札を1枚ずつ DECK に足していく」という意味です。札の表を手で書き並べるのではなく、枚数の数字から自動で作っています。
1枚の札が持つものは3つです。
| 項目 | 役割 |
|---|---|
id |
プログラムが札を見分けるための名前。見た目には出さない |
label |
画面に出す文字 |
power |
強さ。合計して比べる |
id と label を分けるのが要点です。 「1」という表示を「壱」に変えたくなったとき、label だけ直せば済みます。id を表示に使っていると、変えた瞬間にセーブデータも記録も全部壊れます。
そして山の枚数は DECK_SIZE という数字1つから作っています。表を手で書き並べていません。 1〜5が1〜7になるのは、数字を1つ変えるだけです。
勝敗の判定もデータだけを見る
的1つの取り手を決める関数です。盤面のデータ以外、何も見ていません。
function takerOf(side) {
var mine = sumOf(side.mine);
var theirs = sumOf(side.theirs);
if (mine === 0 && theirs === 0) return 'none';
if (mine > theirs) return 'me';
if (theirs > mine) return 'cpu';
return 'none'; // 同点は、いまはどちらのものでもない
}
読み方はこうです。side.mine が自分の置いた札、side.theirs が相手の置いた札で、それぞれの合計を出して大きい方が的を取ります。どちらも置いていなければ none、同点も none を返します。
画面も、手札も、何手目かも見ません。渡された的1つ分のデータだけで答えが出ます。 だから7章でテストが書けますし、6-5で記録から盤面を作り直したときも同じ関数がそのまま使えます。
僕が確かめた結果です。
自分5 対 相手4 → 自分が取る
自分3 対 相手4 → 相手が取る
自分3 対 相手3 → どちらのものでもない
誰も置いていない → どちらのものでもない
「誰も置いていない」を必ず確かめてください。 0対0を「同点だから引き分け」と扱うか「そもそも対象外」と扱うかで、勝敗の数え方が変わります。なお、同点の的を誰のものにするかは6-6で実測して変えます。
やってみる
的の表を作る
HTMLに直接書いていた3つの的を消して、
TARGETSの配列にします。画面に直接書いてあると、的を増やすたびにHTMLを触ることになるからです。データにしてしまえば、以降は表に行を足すだけになります。idから札を引く道具を1つ用意する
function card(id) { for (var i = 0; i < DECK.length; i++) if (DECK[i].id === id) return DECK[i]; throw new Error('知らない札: ' + id); }これ以降、手札も盤面もidの並びで持ちます。札の中身を持ち回すと、同じ札が2枚に増えたりするからです。
見つからないときに
nullを返さず、投げるのが要点です。nullを返すと、あとで「なぜか点数が計算されない」という静かな壊れ方をします。壊れているなら、その場で止まった方が安いです。判定を差し替える
上の
takerOfを入れて、勝敗を見ていた場所を置き換えます。TARGETS.forEach(function (t) { var taker = takerOf(board[t.id]); if (taker === 'me') myTargets++; if (taker === 'cpu') cpuTargets++; });>や<で直接比べていた場所を、全部takerOfへ寄せます。 画面の色付けも、勝敗の集計も、同じ関数を呼びます。ここが分散していると、画面では取ったことになっているのに集計されないという食い違いが出ます。的を1つ足して確かめる
TARGETSに1行足して開いてください。画面の的が4つになって、そのまま遊べたら成功です。 確かめたら消して構いません。保存する
git add . git commit -m "札をデータで持ち、例外ルールを書けるようにした" git push
AIに頼むときの言い方
この作業はAIに任せられます。ただし頼み方で結果が変わります。
claude -p "index.html の的を、HTMLに直接書くのをやめて id と name を持つ TARGETS の配列にして。
画面の描画、盤面の初期化、勝敗の集計、相手の合法手の生成を、全部その配列から作って。
的の数を表す 3 という数字が1箇所も残らないようにして。
札のほうも id・label・power を持つ形にして、山の枚数は数字1つから作って。"
3行目を必ず入れてください。 これが無いと、新しい配列を作ったうえで古い 3 が別の場所に残り、的を増やしたときに画面だけ4つで集計は3つという壊れ方をします。あとから探すのが本当に面倒です。
言い方のコツは、成果物ではなく「残ってはいけないもの」で指定することです。「配列にして」は作る指示、「3が残らないように」は消す指示。後者を書かないと、古い実装は生き残ります。
3-4の「合法手」がここで効きます
3-4で作った legalMoves() を思い出してください。
hand.forEach(function (cardId) {
TARGETS.forEach(function (t) { moves.push({ card: cardId, target: t.id }); });
});
TARGETS をデータにした瞬間、合法手も自動で追従しました。 的を4つにしたら12通りになったのは、これが理由です。
そして札に条件が付いても、直すのはこの関数の中だけです。「この札は3手目以降しか出せない」なら、こう書けます。
{ id: 'ogi', label: '奥義', power: 9, fromTurn: 2 }, // 3手目以降だけ
hand.filter(function (id) {
var c = card(id);
return !c.fromTurn || turn >= c.fromTurn;
}).forEach(function (cardId) { ... });
打てる手を決める場所と、勝敗を決める場所を分ける。 これができていれば、条件は後からいくらでも足せます。
つまずきどころ
的を増やしたら画面だけ増えて、勝敗が合わない
集計が TARGETS を見ていません。画面と集計で別々のものを数えている状態です。両方とも同じ配列を回してください。
idを画面に出してしまった
label を使ってください。id は内部の名前です。画面に出すと、あとで変えられなくなります。
手札に札の中身をそのまま入れた
idの並びで持ってください。中身を入れると、同じ札を2枚持っているのか1枚なのかが曖昧になります。
powerを書き忘れた札がある
undefined は「値が入っていない」という状態を表す言葉です。書き忘れた項目は黙って undefined になり、比較が全部 false になります。気づきにくい壊れ方です。 次の術で、起動時に見つける仕組みを入れます。
データにするほどの量じゃない気がする
的が3つのうちは、そう感じて正しいです。ただし増やす予定があるなら、増える前にやった方が安いです。 増えてから直すと、直す場所が増えた分だけ増えています。
全部データにしたくなってきた
止めてください。「1手番に1枚置く」「合計で比べる」までデータにすると、何のゲームなのかコードから読めなくなります。 データにするのは増えるものだけ、が線引きです。
Windows の場合
コードは同じです。
次の一歩
TARGETS に { id: 't4', name: '四の的' } を1行足して開いてみてください。
的が4つになって、そのまま遊べます。判定のコードも、相手の思考も、1文字も触っていません。
そのうえで、「これは本当に面白くなったか」も見てください。的が4つになると1つあたりの重みが減って、捨てる痛みが薄れます。データで簡単に増やせるからこそ、増やすかどうかは別の判断です。 その判断は6-6と6-7でやります。
データで持つ形になりました。次は実際に中身を増やして、増えたときに何が壊れるかを見ます!
ここまでの三つの的
この講座は、見本ゲーム「三つの的」を術ごとにすこしずつ育てながら進みます。下にあるのはこの術を終えた時点の実物です。そのまま遊べます。