フォーラインの実装を解説
6行7列の盤面、駒を落とす処理、縦・横・斜めの4連判定、難易度別CPU、キーボード操作、アニメーション、BGM・効果音まで、ゲームページの実装を順番に紹介します。
フォーラインは、7列6段の盤面に駒を落とし、縦・横・斜めのいずれかで先に4つをつなげるゲームです。ルールはシンプルですが、列の一番下から空きを探す処理、直前の着手を起点にした連結判定、CPUの手番、入力を一時的に止める状態管理など、ブラウザゲームの基本をまとめて学べます。
ゲームページはPHPで共通ヘッダーなどを読み込み、HTMLで操作パネルと盤面の入れ物を用意しています。ゲームの状態変更と描画、CPUの思考、音声はJavaScriptで処理し、CSSは駒の落下と4連の発光を表現する役割に分けています。
盤面を6行7列の配列で管理する
盤面は6行7列の二次元配列です。空きマスを0、プレイヤーの黄色い駒を1、CPUの赤い駒を2としておくと、駒の有無と所有者を同じ値で管理できます。
const ROWS = 6;
const COLS = 7;
function createBoard() {
return Array.from(
{ length: ROWS },
() => Array(COLS).fill(0)
);
}
let board = createBoard();行番号は上から下へ、列番号は左から右へ進みます。描画するときもこの順でマスを生成するため、画面の配置と配列の座標が一致します。beginやresetで新しい配列を作れば、前の対戦の駒を残さず盤面を初期化できます。
空いている一番下の行を探す
プレイヤーは列を選ぶだけで、駒の位置を直接指定しません。選んだ列の最下段から上へ調べ、最初に見つかった空きマスへ駒を置きます。列の上まで埋まっている場合は、-1を返して着手できないことを表します。
function findRow(col) {
for (let row = ROWS - 1; row >= 0; row -= 1) {
if (board[row][col] === 0) return row;
}
return -1;
}この関数で「重力」を表現できます。上から空きを探すのではなく、下から探すことで、駒が途中で浮かずに積み上がります。全列の一番上が埋まっているかを調べれば、盤面が満杯かどうかも判定できます。
function validColumns() {
return Array.from({ length: COLS }, (_, col) => col)
.filter(col => board[0][col] === 0);
}
function isFull() {
return validColumns().length === 0;
}列の最上段が空いていれば、その列にはまだ駒を置けます。この一覧は、プレイヤーの入力確認だけでなく、CPUが選べる列の絞り込みにも使い回しています。
4方向だけを調べて4連を判定する
新しい駒を置いたとき、盤面全体を毎回調べる必要はありません。勝敗に影響するのは直前に置いた駒を含む連結だけなので、そのマスを起点に、横、縦、右下方向、左下方向の4方向を確認します。
function lineFrom(row, col, dr, dc, token) {
const line = [[row, col]];
for (const direction of [-1, 1]) {
let r = row + dr * direction;
let c = col + dc * direction;
while (
r >= 0 && r < ROWS &&
c >= 0 && c < COLS &&
board[r][c] === token
) {
line.push([r, c]);
r += dr * direction;
c += dc * direction;
}
}
return line;
}
function winningLine(row, col, token) {
for (const [dr, dc] of [
[0, 1], [1, 0], [1, 1], [1, -1]
]) {
const line = lineFrom(row, col, dr, dc, token);
if (line.length >= 4) return line;
}
return null;
}lineFromは、指定した方向の両側へ進みます。たとえば横方向なら、置いた駒から左と右を調べて一つのラインにまとめます。中央の駒を起点にすることで、左側と右側に分かれていた同じ色の駒も合計して判定できます。盤面の外へ出ないように、行と列の範囲を毎回確認しています。
駒の追加と4連の成立を分ける
着手処理では、まず盤面の値を更新し、その直後に落下表示を行います。少し待ってから勝敗を調べることで、駒が落ちてから4連が光る順番になります。判定中はbusyをtrueにして、アニメーション中の連続入力を受け付けません。
function dropPiece(row, col, token) {
board[row][col] = token;
busy = true;
renderBoard({ row, col });
window.setTimeout(() => {
const line = winningLine(row, col, token);
finishMove(token, line);
}, 460);
}renderBoardへ落としたマスをdropとして渡すため、その駒だけにis-newクラスを付けられます。判定結果のラインは、勝利時の再描画へwinningとして渡します。ゲームのルールと演出に必要な情報を、同じ状態から別々に利用している点がポイントです。
追加された駒と勝利した4連を表示で区別する
画面上では、新しく追加された駒を落下アニメーションで表し、4連になった駒を発光させています。ゲーム内部ではどちらも同じ駒ですが、描画関数へ異なる情報を渡すことで、プレイヤーが何が起きたのかを理解しやすくしています。
function renderBoard(drop = null, winning = []) {
const winningSet = new Set(
winning.map(([row, col]) => `${row}:${col}`)
);
boardEl.replaceChildren();
for (let row = 0; row < ROWS; row += 1) {
for (let col = 0; col < COLS; col += 1) {
const piece = document.createElement('span');
piece.className = 'cf-piece';
if (board[row][col] === 1) piece.classList.add('player');
if (board[row][col] === 2) piece.classList.add('cpu');
if (drop?.row === row && drop?.col === col) {
piece.classList.add('is-new');
}
if (winningSet.has(`${row}:${col}`)) {
piece.classList.add('is-winning');
}
}
}
}実際の描画では、マスをボタンとして生成し、その中に駒のspanを追加しています。盤面を毎回作り直す方式なので、前の勝利表示や古い駒のクラスが残りません。CSSではis-newの落下とis-winningの発光を別々に定義しています。
CPUは難易度ごとに選び方を変える
CPUの初級は合法な列からランダムに選びます。中級以上では、まずCPU自身が次の一手で勝てる列を探し、その次にプレイヤーの4連を止める列を探します。上級では中央付近の列を選びやすくし、単純なランダムよりも中央を使う対戦になります。
function findCpuMove() {
const columns = validColumns();
if (difficultyEl.value === 'easy') {
return columns[Math.floor(Math.random() * columns.length)];
}
const win = findTacticalMove(2);
if (win >= 0) return win;
const block = findTacticalMove(1);
if (block >= 0) return block;
if (difficultyEl.value === 'hard') {
const center = columns.filter(col => Math.abs(col - 3) <= 1);
const pool = center.length ? center : columns;
return pool[Math.floor(Math.random() * pool.length)];
}
const weighted = columns.flatMap(col =>
Array(4 - Math.abs(col - 3)).fill(col)
);
return weighted[Math.floor(Math.random() * weighted.length)];
}findTacticalMoveは、候補の列へ一時的に駒を置き、winningLineで4連が成立するかを調べます。調べ終わったら空きマスへ戻すため、実際の着手前に盤面を壊しません。CPU専用の複雑な勝利判定を作らず、プレイヤーと同じ4連判定を再利用しています。
手番と入力ロックを状態で管理する
runningは対戦中か、busyは駒のアニメーションやCPUの手を待っているか、gameOverは終了したかを表します。クリック処理の入口でこれらを確認するため、終了後やCPUの思考中にプレイヤーが駒を追加することを防げます。
function chooseColumn(col) {
if (!running || busy || gameOver) return;
const row = findRow(col);
if (row < 0) {
messageEl.textContent =
'その列はいっぱいです。別の列を選んでください。';
tone(130, .06, 'sawtooth');
return;
}
dropPiece(row, col, 1);
}プレイヤーの着手後は、CPUの手が終わるまでbusyを維持します。CPUの着手が終わったらbusyを解除し、メッセージと盤面を更新します。引き分けでは、isFullが真になった時点で終了処理へ進みます。
キーボードとタッチに対応する
画面の列ボタンをクリックできるだけでなく、数字キー1~7で列を選び、左右キーでフォーカスを移動し、EnterまたはSpaceで駒を落とせます。各マスをボタンとして作っているため、タッチ操作にも対応しやすく、キーボード操作では現在の列へフォーカスを戻せます。
document.addEventListener('keydown', event => {
if (/^Digit[1-7]$/.test(event.code)) {
selectedColumn = Number(event.code.slice(-1)) - 1;
chooseColumn(selectedColumn);
}
if (event.code === 'ArrowLeft' || event.code === 'ArrowRight') {
event.preventDefault();
selectedColumn = Math.max(
0,
Math.min(COLS - 1, selectedColumn +
(event.code === 'ArrowRight' ? 1 : -1))
);
boardEl.querySelector(
`[data-column="${selectedColumn}"]`
)?.focus();
}
});HTML側のセルには列番号をdata-columnとして持たせ、ARIAラベルにも「何列何段か」を設定しています。見た目だけでなく、キーボードと読み上げでも盤面の位置を把握できるようにしています。
Web Audio APIでBGMと効果音を作る
BGMと効果音は外部音声ファイルではなく、Web Audio APIの発振器から生成しています。ブラウザの自動再生制限に対応するため、ゲーム開始ボタンなどユーザー操作の中でAudioContextを作成し、再生を始めます。
function tone(freq, duration = .08, type = 'square', volume = .13) {
if (!audio || audio.state !== 'running' || !sfxOn) return;
const now = audio.currentTime;
const oscillator = audio.createOscillator();
const gain = audio.createGain();
oscillator.type = type;
oscillator.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(.0001, now);
gain.gain.exponentialRampToValueAtTime(volume, now + .01);
gain.gain.exponentialRampToValueAtTime(.0001, now + duration);
oscillator.connect(gain).connect(master);
oscillator.start(now);
oscillator.stop(now + duration + .03);
}プレイヤーとCPUで周波数を変え、勝利時と敗北時には複数の音を少しずつずらして鳴らします。BGMは一定間隔のタイマーで音階を切り替え、BGMと効果音を別々のスイッチで停止できるようにしています。画面を離れたときはBGMを止めるため、戻ったときに意図しない再生が続きません。
実装のポイント
フォーラインでは、盤面、手番、入力ロック、勝利ライン、音声をそれぞれ役割の異なる状態として管理しています。特に重要なのは、駒の位置を決めるfindRowと、勝利を調べるwinningLineをプレイヤーとCPUの両方で使い回すことです。
ゲーム画面では、駒の追加を落下アニメーション、4連の成立を発光で表現しています。ルール判定と描画用の情報を分けることで、見た目の演出を増やしてもゲームの状態を壊しにくくなります。実際のフォーラインでは、まず中央の列を使いながら、相手の次の4連を止める手を探してみてください。