icon
icon

JavaScriptでオセロゲームを作成する方法を現役エンジニアが解説【初心者向け】

初心者向けにJavaScriptでオセロゲームを作成する方法について現役エンジニアが解説しています。まずはコーディング仕様を明確にしておくことが重要です。その後で初期画面や石が置けるかどうかのチェックや全体の流れに落とし込んでいきます。

テックアカデミーマガジンは受講者数No.1のプログラミングスクール「テックアカデミー」が運営。初心者向けにプロが解説した記事を公開中。現役エンジニアの方はこちらをご覧ください。 ※ アンケートモニター提供元:GMOリサーチ株式会社 調査期間:2021年8月12日~8月16日  調査対象:2020年8月以降にプログラミングスクールを受講した18~80歳の男女1,000名  調査手法:インターネット調査

監修してくれたメンター

高田 悠

JavaScriptを用いた実装などフロントエンド領域の開発が得意。Web上での3D表現に興味がありWebARの実装案件を複数経験。ワークライフバランスを重視してフリーランス生活を送っている。

JavaScriptでオセロゲームを作成する方法について、テックアカデミーのメンター(現役エンジニア)が実際のコードを使用して初心者向けに解説します。

実際のコードをもとに解説していきますので、理解を深めていきましょう。

目次

そもそもJavaScriptについてよく分からないという方は、JavaScriptとは何なのかについて解説した記事を読むとさらに理解が深まります。

 

田島悠介

今回は、JavaScriptに関する内容だね!

大石ゆかり

どういう内容でしょうか?

田島悠介

JavaScriptでオセロゲームを作成する方法について詳しく説明していくね!

大石ゆかり

えっ?オセロゲームを作るなんて、難しそうですね、、、

田島悠介

大丈夫!1つ1つの処理を丁寧に解説するから、一緒に頑張ろう。

大石ゆかり

はい!よろしくお願いします!

 

はじめに

対象読者

本記事は、JavaScriptを学習中であり、変数や関数などひととおりの基礎知識を学習した初心者の方向けに書いています。

本記事が難しすぎると感じた場合や、JavaScriptの基礎を学習してから挑戦したい場合には、以下で紹介しているサイトなどを用いて学習してみましょう!

入門向けのJavaScriptを学習できるサイト

 

本記事の目的

本記事の目的は、JavaScriptを用いたオセロゲームの実装を通して、以下のようなスキルを習得・向上させることです。

  • 設計から実装までの、エンジニアの思考プロセスを体験する
  • ゲームロジックに基づいたプログラム実装を通して、論理的思考能力を鍛える
  • オセロゲームの実現に必要な様々な処理を学び、JavaScriptという言語の仕様をより深く理解する

上記の目的を達成するために、本記事は読者のみなさんが実際に手を動かしながら学べるハンズオン形式をとっています。

解説に沿ってコードを書きながら、プログラミングに親しんでいきましょう!

 

注意点

本記事では、オセロゲームを0から実装するために必要な解説のすべてを記載しているため、記事の分量が多くなっています。

一気に最後まで進めようとせずに、ご自身のペースで学習を進めることを推奨します。

本記事では章ごとに所要時間の目安を記載しているので、学習の区切りを決める参考にしてください。

 

オセロゲームのルールに基づいて設計をしよう

🕛目安時間:1時間

ゲームに限らず、いかなる実装においても設計は非常に重要です。

機能要件を満たしつつも、できるだけシンプルでメンテナンスしやすい設計を考えるのもエンジニアの醍醐味の1つです。

「その設計、自分ならどうするかな?」ということを考えながら読み進めてみてください。

 

オセロゲームのルールを確認しよう

オセロゲームを実装する上での機能要件は、一言でいうと「オセロゲームのルールに従って盤面の状態が変えられる」ということです。

まずは前提条件となるオセロゲームのルールを確認していきます。

 

オセロゲームのルール

  • 盤は8×8マスを利用する
  • 初期状態で真ん中の4マスに白黒の石を交互に配置する
  • 先攻を黒とし、黒⇨白⇨黒…の順番で石を置いていく
  • 石を置ける場所は、下記の条件を満たしている
    1. 置くマスに他の石がないこと
    2. 置くマスの上下左右、斜め1マスの場所に相手の石があること
    3. 置いた石、相手の石があった方向の延長上に自身の石があること
  • 石を置ける条件を満たせない場合は、パスをして相手の番になる
  • すべてのマスに石が置かれた時点でゲーム終了(「空きがあるが、白黒どちらも置けない」状況は今回は考えないことにする)
  • ゲーム終了時点で石の数が多い方が勝ちとなる

以上がオセロゲームのルール概要です。

 

ルール以外の要件を定めよう

ゲームのルールのほか、今回の実装では以下の条件を適用することとします。

  • 自動でパスする機能は実装せず、「パス」ボタンを設置する
  • 「現在どちらの手番であるか」を表示する
  • ゲーム終了後、白黒それぞれの石の数と勝敗を表示する
  • ゲームの初期化機能は実装しない
  • 石が裏返るなどのアニメーションは実装しない
  • CPUとの対戦機能は実装しない
  • jQueryなどのフレームワークは使用しない
  • 画像は用いない

それでは、要件を確認したところで実際の設計に移りましょう。

 

要件に沿って実装設計をしてみよう

同じルールや条件でも、それをどのように実装に落とし込むかはエンジニア次第です。

実装設計では、いかに無駄なくすべての要件を満たした実装ができるかを考え尽くします。

それでは、紙やプレゼンスライドなどに、大まかな処理の流れを考えて書いてみてください。

想定されるケースを洗い出し、そのケースに応じてどう処理を分岐するかを矢印で表してみましょう。

 

本記事の執筆者は、以下のように流れを作成しました。

上図のように場合分けをしつつ、プログラムのフローを書き出すことで、コードに落とし込む際に整理がしやすくなります。

もちろん上記のフロー図が唯一の正解ということではなく、他にもいろいろなパターンの表現方法が考えられます。

想定されるケース・条件を網羅し、かつ無駄のない流れで設計できると、「良いフロー図」と呼ぶことができます。

本セクションは以上です。

休憩・復習などをして、ご自身のペースで次のセクションへ進んでください。

 

[PR] コーディングで副業する方法とは

開発環境を整えよう

🕛目安時間:30分

今回の開発に必要な環境は以下の通りです。

  • コードエディタ(図解ではVisual Studio Codeを使用します)
  • Webブラウザ(Google Chrome推奨)

 

コードエディタを用意しよう

コードエディタは普段使い慣れているもので構いませんが、解説に用いるVisual Studio Codeを導入したい方は、以下の記事を参考にセットアップしてください。

Visual Studio Code(VSCode)とは?インストールや使い方も現役エンジニアが解説

 

Webブラウザの環境を整備しよう

主要なWebブラウザには、コードの構造を確認したり、エラーを発見したりするための開発者用ツールが備えられています。

本記事では、実務現場の多くで使われているGoogle Chromeを用いて開発を進めていきます。

開発者ツールの使い方をまだ学習していない方は、次のセクションに進む前に以下の記事に目を通しておいてください。

Chromeの開発者ツールを使ってJavaScriptのデバッグを行う方法を現役エンジニアが解説【初心者向け】

盤面と石をブラウザ上に表示しよう

🕛目安時間:1時間30分

このセクションでは、オセロゲームに必要な盤および石を表示するための実装を行います。

本格的にHTML / CSS / JavaScriptのコードが登場するので、要点を1つ1つ理解しながらコーディングをしていきましょう。

 

必要なフォルダとファイルを作成しよう

まずはプロジェクトのルートとなるフォルダを1つ、PCの任意の場所に作成してください。
(例ではオセロゲームの一般的な英語名であるreversiというフォルダ名にしました)

 

フォルダ内には、以下の3つのファイルを作成してください。

  • index.html
  • style.css
  • main.js

下図のような構成にできていればOKです。

 

HTMLをコーディングしよう

いよいよコーディング作業に入ります!

今回はサンプルコードに従って、index.htmlの中身を記述してみましょう。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>オセロゲーム</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- 盤 -->
  <div id="stage" class="stage"></div>
  <!-- 各マスを描画するためのテンプレート -->
  <div id="square-template" class="square">
    <div class="stone"></div>
  </div>
  <script src="main.js"></script>
</body>
</html>

盤全体を表すstageという要素と、64個のマスを描画するためのテンプレートの役割を持つ要素を記述しました。

テンプレートの要素はそれ自体を使うのではなく、JavaScriptを用いて要素をコピーすることで、同じ構成をしたHTMLの繰り返し描画を実現します。

勝敗を表示するための要素や、現在の手番を表示するための要素は、後ほど追加していきます。

 

CSSをコーディングしよう

HTMLだけではオセロゲームの格子状のデザインは実現できないので、CSSで見た目を調整していきます。

本記事の趣旨はJavaScriptのロジック実装であるため、CSSに関する解説は割愛します。

以下のCSSコードをstyle.cssに記述しましょう。

*, *::before, *::after {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  background-color: #008000;
}

.stage {
  display: flex;
  flex-wrap: wrap;
  margin: 60px;
  width: 404px;
  height: 404px;
}

.square {
  position: relative;
  width: 50px;
  height: 50px;
  border: solid black;
  border-width: 0 4px 4px 0;
  cursor: pointer;;
}

.square:nth-child(-n + 8) {
  border-width: 4px 4px 4px 0;
  height: 54px;
}

.square:nth-child(8n + 1) {
  border-width: 0 4px 4px 4px;
  width: 54px;
}

.square:first-child {
  border-width: 4px;
  width: 54px;
  height: 54px;
}

.stone {
  position: absolute;
  top: 2px;
  bottom: 0;
  left: 2px;
  width: 42px;
  height: 42px;
  border-radius: 21px;
  background-color: black;
}

#square-template {
  display: none;
} 

HTMLとCSSを記述しましたが、現時点でindex.htmlをブラウザに読み込ませてみても、緑色の背景しか表示されません。

64マスの枠と石については、これからJavaScriptを駆使して表示していきます。

 

JavaScriptで64マスの枠と石を表示しよう

さきほど、HTMLに以下のようにテンプレート要素の記述をしました。

<!-- 各マスを描画するためのテンプレート -->
<div id="square-template" class="square">
  <div class="stone"></div>
</div>

 

これから、テンプレート要素をマスの数である64回クローンして、親要素となるdiv要素に追加していきます。

main.jsに以下のように記述してください。

const stage = document.getElementById("stage");
const squareTemplate = document.getElementById("square-template");

const createSquares = () => {
  for (let i = 0; i < 64; i++) {
    const square = squareTemplate.cloneNode(true); //テンプレートから要素をクローン
    square.removeAttribute("id"); //テンプレート用のid属性を削除
    stage.appendChild(square); //マス目のHTML要素を盤に追加
  }
};

window.onload = () => {
  createSquares();
};

createSquares関数に注目してみましょう。

64回分のループは、シンプルなfor文で記述しています。

ループ内では、まずcloneNodeという組み込み関数(JavaScriptに最初からある関数)を使い、テンプレート要素のクローンを作成しています。

テンプレート要素にはsquare-templateというidが振られていますが、そのままにしておくと64個のクローン要素にも同じidがついてしまうため、同じく組み込み関数のremoveAttributeを使いidを削除しました。

そして、調整したクローン要素を、親要素となるidがstageのdiv要素に追加しています。

 

JavaScriptまで記述できたら、index.htmlファイルをブラウザに読み込ませて、表示を確認してみましょう。

正しく記述できていれば、上図のようにすべてのマスに黒石が置かれたような状態になります。

もし表示が崩れていたら、HTML / CSS / JavaScript のいずれかの記述にミスがあります。

 

HTML / CSSのミスを探す際はブラウザ開発者ツールの「要素」タブを使用し、想定した箇所にスタイルが当たっているかを確認するようにしましょう。

 

また、JavaScriptのミスを探す際は、開発者ツールの「コンソール」タブをチェックしてみましょう。

画像はわざとタイプミスをして表示させたエラー出力の一例です。

JavaScriptでの開発中は特に開発者ツールを逐一確認して、エラーをこまめにつぶすようにしましょう。

 

盤面の状態をゲームの初期状態にしよう

セクション1で確認した通り、オセロゲームの初期状態は中央4マスに白と黒の石が交互に置いてある状態です。

中央にだけ石が置かれた状態になるように、CSSおよびJavaScriptを編集していきます。

石を表すHTML要素に対するCSSは、現状以下のようになっています。

.stone {
  position: absolute;
  top: 2px;
  bottom: 0;
  left: 2px;
  width: 42px;
  height: 42px;
  border-radius: 21px;
  background-color: black; //石の色が黒色で固定されたままになっている
}

 

ゲーム中の石の状態は、以下の3パターンに分けられます。

  • 置かれていない(非表示)
  • 黒色である
  • 白色である

従って、この3つの状態にそれぞれ対応するスタイルを記述する必要があります。

クラス名でスタイルを分岐してもよいのですが、JavaScriptで管理しやすくするため、今回は「data属性」の値に応じた分岐にしてみます。

※data属性については、以下の記事で詳しく解説されています。

JavaScriptのdata属性の使い方を現役エンジニアが解説【初心者向け】

 

それでは、CSSの該当部分を以下のように編集してみましょう。

.stone {
  position: absolute;
  top: 2px;
  bottom: 0;
  left: 2px;
  width: 42px;
  height: 42px;
  border-radius: 21px;
}

.stone[data-state="0"] {
  display: none;
}

.stone[data-state="1"] {
  background-color: black;
}

.stone[data-state="2"] {
  background-color: white;
}

data属性のdata-stateが0なら非表示、1なら黒色、2なら白色になるよう、スタイルを記述しました。

あとはJavaScript側でdata-stateを書き換えていくことで、自動的に3パターンのスタイルが切り替わっていきます。

 

では、次にJavaScriptです。

中央4マスの石を操作するアプローチは複数考えられますが、今回はもっともシンプルなif文を使った方法で実装してみます。

まず、64回ループするfor文において、中央4マスを通る時のインデックスの値は27, 28, 35, 36です。

さらに、27と36の時は黒色、28, 35の時は白色、とそれぞれ石を置くことで、初期配置を再現できます。

 

それでは、if文を用いて石の初期配置をコードに起こしてみましょう。

main.jsのcreateSquares関数を以下のように編集しましょう。

const createSquares = () => {
  for (let i = 0; i < 64; i++) {
    const square = squareTemplate.cloneNode(true);
    square.removeAttribute("id");
    stage.appendChild(square);

    const stone = square.querySelector('.stone');

    //ここから編集
    let defaultState;
    //iの値によってデフォルトの石の状態を分岐する
    if (i == 27 || i == 36) {
      defaultState = 1;
    } else if (i == 28 || i == 35) {
      defaultState = 2;
    } else {
      defaultState = 0;
    }

    stone.setAttribute("data-state", defaultState);
  }
};

組み込み関数のsetAttributeを使えば、要素のdata属性の値を設定できます。

data属性の値を分岐することで、初期配置の石の状態になるようにスタイルが適用されるという仕組みです。

 

それでは、index.htmlを再読み込みしてみましょう。

図のように表示されていれば成功です!

見慣れたオセロゲームの見た目になってきましたね。

 

本章は以上です。

長い章でしたが、最後までがんばりましたね。

リフレッシュをして次に進みましょう。

 

クリックしたマスに石を置けるかを判定しよう

🕛目安時間:2時間

石を置いてゲームを進める前に、選択したマスに石を置けるかの判定ロジックを実装します。

本記事で最も難しく、論理的思考力を鍛えることができる章なので、集中してがんばっていきましょう。

 

各マスの石の状態をどう管理するか

前章で、HTML要素のdata属性の値が0なら置かれていない、1なら黒、2なら白という取り決めをしました。

しかし、石を置けるかの判定の際には多くのマスの状態を取得する必要があるので、毎回HTML要素の値を取得する処理では少し無駄があります。

そこで、石の状態を高速に参照できるように配列を生成し、0, 1, 2の数値をマスごとに格納することにします。

main.jsを以下のように編集してください。

const stage = document.getElementById("stage");
const squareTemplate = document.getElementById("square-template");
//追加
const stoneStateList = [];

const createSquares = () => {
  for (let i = 0; i < 64; i++) {
    const square = squareTemplate.cloneNode(true);
    square.removeAttribute("id");
    stage.appendChild(square);

    const stone = square.querySelector('.stone');

    //ここから編集
    let defaultState;
    //iの値によってデフォルトの石の状態を分岐する
    if (i == 27 || i == 36) {
      defaultState = 1;
    } else if (i == 28 || i == 35) {
      defaultState = 2;
    } else {
      defaultState = 0;
    }

    stone.setAttribute("data-state", defaultState);
    //ここから追加
    stone.setAttribute("data-index", i); //インデックス番号をHTML要素に保持させる
    stoneStateList.push(defaultState); //初期値を配列に格納
  }
};

window.onload = () => {
  createSquares();
};

コードの冒頭で配列の定義と、createSquares関数の最後で配列に初期値の格納を行いました。

また、配列のインデックス番号と、配列要素に対応するHTML要素をひもづけるために、data-indexというdata属性を追加しています。

配列の追加によって、例えば一番右上のマスの状態を取得したい場合は、stoneStateList[0]とすればよくなりました。

この配列の状態は、盤面の状態が変わるごとに必ず同期させる必要があることに留意しましょう。

 

マスをクリックしたイベントを取得する

オセロゲームはユーザーがあるマスをクリックするというアクションによって進行します。

「マスをクリックする」という操作を検知するには、addEventListenerを使います。

main.jsに以下のように追記してください。

なお、一部の記述を省略していますが、案内がない限りはこれまで書いたコードを削除する必要はありません。

//createSquaresの上に、関数を新しく作る
const onClickSquare = (index) => {
  console.log(index)
}

const createSquares = () => {
  for (let i = 0; i < 64; i++) {
    //省略

    //for文の最後に追記
    square.addEventListener('click', () => {
      onClickSquare(i);
    })
  }
};

それぞれのマスに対応するHTML要素に、クリックイベントを取得するaddEventListenerでイベントハンドラを登録しました。

クリックを検知すると、onClickSquare関数を呼び出します。

onClickSquare関数には引数として、石の状態を取得するための配列の参照インデックスを渡しています。

試しにindex.htmlでマスをクリックすると、コンソールにはインデックス番号が出力されるはずです。

 

石を置ける条件

選択したマスに石を置ける条件は、大きく分けると以下の2つです。

  • そのマスに他の石がないこと
  • そのマスに石を置いたときに、1つでも相手の石をひっくり返せること

「そのマスに他の石がないこと」は容易に判定ができますが、「そのマスに石を置いたときに、1つでも相手の石をひっくり返せること」については、細分化して条件を検討する必要があります。

それでは実際に条件をコードに落とし込んでいきましょう。

 

そのマスに他の石がないことの判定

クリックされたマスに石があるかどうかは、そのマスの状態を表す値が0であることを確認することで判定ができます。

main.jsのonClickSquare関数のconsole.logを消して、代わりに下記の記述を追加してください。

const onClickSquare = (index) => {
  if (stoneStateList[index] !== 0) {
     alert("ここには置けないよ!");
     return;
   }
}

 

クリックしたマスに対応する配列要素が0でない場合は、アラートで置けないことをユーザーに伝えるようにしています。

ブラウザを開き、中央4マスの石が置かれた部分をクリックすることで、この状況が再現できます。

 

そのマスに石を置いたときに、1つでも相手の石をひっくり返せること

本項目はやや難しいので、集中できるときに取り組むことをおすすめします。

アプローチとしては、返せる石のリストを取得できる処理を作成し、そのリストの中身があれば石は置ける、なければ石は置けないということを判定していきます。

下図のような流れで判定すると、返せる石をすべて取得できます。

図のロジックに沿って、1つ具体的に検証してみましょう。

図の赤星の位置に黒石を置いた時、左方向に返せる石が存在するかを判定します。

[1]隣にマスがあるか→あるので、[2]に進みます。

[2]隣に石があるか→あるので、[3]に進みます。

[3]その石が相手の石の色か⇨1つ隣の石は白で相手の色なので、ここで仮ボックスにこの白石の情報を入れておきます。

[4]さらに延長線上に石があるか⇨白石の左には石があるので、[5]に進みます。

[5]延長線上の石が相手の色か⇨白石の左の石は黒なので、自分の色となり、ここで仮ボックスに入れた白石を返せることが確定します。

[4]と[5]は、返せる石が確定するまでループすることがポイントです。

この時点でなんとなく、「[1]〜[3]のコードを書いた後、[4]と[5]をfor文ループにするのかな」と、コードの大枠を想像できると最高です!

 

作成したロジックをコードに落とし込もう

それでは図のロジックをコードに起こしてみましょう。

※下記のコードはやや複雑な計算を含んでいるため、コピーアンドペーストでも構いません。

コピーアンドペーストをする場合は、ポイントとなるfor文の中身に関してはなるべく理解できるよう読んでみてください。

//onClickSquare関数のすぐ上に記述しましょう
let currentColor = 1;
const getReversibleStones = (idx) => {
  //クリックしたマスから見て、各方向にマスがいくつあるかをあらかじめ計算する
  //squareNumsの定義はやや複雑なので、理解せずコピーアンドペーストでも構いません
  const squareNums = [
    7 - (idx % 8),
    Math.min(7 - (idx % 8), (56 + (idx % 8) - idx) / 8),
    (56 + (idx % 8) - idx) / 8,
    Math.min(idx % 8, (56 + (idx % 8) - idx) / 8),
    idx % 8,
    Math.min(idx % 8, (idx - (idx % 8)) / 8),
    (idx - (idx % 8)) / 8,
    Math.min(7 - (idx % 8), (idx - (idx % 8)) / 8),
  ];
  //for文ループの規則を定めるためのパラメータ定義
  const parameters = [1, 9, 8, 7, -1, -9, -8, -7];

  //ここから下のロジックはやや入念に読み込みましょう
  //ひっくり返せることが確定した石の情報を入れる配列
  let results = [];

  //8方向への走査のためのfor文
  for (let i = 0; i < 8; i++) {
    //ひっくり返せる可能性のある石の情報を入れる配列
    const box = [];
    //現在調べている方向にいくつマスがあるか
    const squareNum = squareNums[i];
    const param = parameters[i];
    //ひとつ隣の石の状態
    const nextStoneState = stoneStateList[idx + param];

    //フロー図の[2][3]:隣に石があるか 及び 隣の石が相手の色か -> どちらでもない場合は次のループへ
    if (nextStoneState === 0 || nextStoneState === currentColor) continue;
    //隣の石の番号を仮ボックスに格納
    box.push(idx + param);

    //フロー図[4][5]のループを実装
    for (let j = 0; j < squareNum - 1; j++) {
      const targetIdx = idx + param * 2 + param * j;
      const targetColor = stoneStateList[targetIdx];
      //フロー図の[4]:さらに隣に石があるか -> なければ次のループへ
      if (targetColor === 0) continue;
      //フロー図の[5]:さらに隣にある石が相手の色か
      if (targetColor === currentColor) {
        //自分の色なら仮ボックスの石がひっくり返せることが確定
        results = results.concat(box);
        break;
      } else {
        //相手の色なら仮ボックスにその石の番号を格納
        box.push(targetIdx);
      }
    }
  }
  //ひっくり返せると確定した石の番号を戻り値にする
  return results;
};

 

コードの意図はコメントで書いている通りですが、いくつか補足しておきましょう。

コードの構成として、for文の中にさらにfor文が入っています。

外側のfor文は、右、右下、下、左下、左、左上、上、右上と、8方向にひっくり返す石の判定を行うためのものです。

for (let i = 0; i < 8; i++) {

 

内側のfor文は、各方向にあるマスごとの判定用であり、ループの回数はあらかじめ定義したマスの数に応じて定めています。

for (let j = 0; j < squareNum - 1; j++) {

 

また、for文内でのbreakとcontinueの使い分けがポイントになっています。

breakはその時点でfor文自体を終了するのに対して、continueはfor文自体は継続し、次のループに処理を移行する機能を持っています。

最後に、getReversibleStones関数は、返り値として「ひっくり返せる石のインデックス番号の配列」を返します。

石の番号というミニマムの情報を取得し、その後実際にひっくり返す処理などを、次の章で実装していきましょう。

 

石の反転と手番の交代、ゲーム終了の判定を実装しよう

🕛目安時間:1時間

前章までで、オセロゲームのロジックのメインとなる「ひっくり返す石の判定」が実装できました。

本章では返せる石の情報を取得した後の、オセロゲーム進行のための処理を実装します。

処理の流れは以下の通りです。

  1. 実際に盤面上の石の色を反転させる
  2. 手番が白なら黒、黒なら白に交代する
  3. もし盤面が石で埋まっていれば、計測してゲームを終了する

上記処理の実装にあたって、現在の手番を表示するためのHTML要素の追加を行います。

以下のようにbodyタグ内に要素を追記してください。

<body>
  <!-- 現在の手番を表示する -->
  <p>現在の手番は<span id="current-turn">黒</span>です</p>

  <!-- 以下省略 -->
</body>

 

次に、main.jsの処理です。

//getReversibleStones関数のすぐ上に書きましょう
const currentTurnText = document.getElementById("current-turn");
//関数の中身を以下のように編集しましょう
const onClickSquare = (index) => {
  //ひっくり返せる石の数を取得
  const reversibleStones = getReversibleStones(index);

  //他の石があるか、置いたときにひっくり返せる石がない場合は置けないメッセージを出す
  if (stoneStateList[index] !== 0 || !reversibleStones.length) {
    alert("ここには置けないよ!");
    return;
  }

  //自分の石を置く 
  stoneStateList[index] = currentColor;
  document
    .querySelector(`[data-index='${index}']`)
    .setAttribute("data-state", currentColor);

  //相手の石をひっくり返す = stoneStateListおよびHTML要素の状態を現在のターンの色に変更する
  reversibleStones.forEach((key) => {
    stoneStateList[key] = currentColor;
    document.querySelector(`[data-index='${key}']`).setAttribute("data-state", currentColor);
  });

  //もし盤面がいっぱいだったら、集計してゲームを終了する
  if (stoneStateList.every((state) => state !== 0)) {
    const blackStonesNum = stoneStateList.filter(state => state === 1).length;
    const whiteStonesNum = 64 - whiteStonesNum;

    let winnerText = "";
    if (blackStonesNum > whiteStonesNum) {
      winnerText = "黒の勝ちです!";
    } else if (blackStonesNum < whiteStonesNum) {
      winnerText = "白の勝ちです!";
    } else {
      winnerText = "引き分けです";
    }

    alert(`ゲーム終了です。白${whiteStonesNum}、黒${blackStonesNum}で、${winnerText}`)
  }

  //ゲーム続行なら相手のターンにする
  currentColor = 3 - currentColor;

  if (currentColor === 1) {
    currentTurnText.textContent = "黒";
  } else {
    currentTurnText.textContent = "白";
  }
}

 

当初の設計通り、「すでに石が置いてあるか、ひっくり返せる石がない = そのマスには置けない」という条件をif文で表現しています。

if (stoneStateList[index] !== 0 || !reversibleStones.length) {

このif文で処理が終わらなければ、1つ以上石がひっくり返せるということなので、その次の処理で盤面の状態を変更しています。

石をひっくり返す際の処理は「HTML要素のdata属性の値を変更する」「配列stoneStateListの要素を更新する」の2つです。

石の反転処理を終えたら、次はゲームがそこで終了かどうかの判定をしています。

もし盤面の石の数が64であれば、その時点で集計し、ユーザー向けに勝敗のメッセージを出しています。

 

盤面の石の数が64でなければ、手番を入れ替えています。

手番入れ替えのコードにはちょっとした工夫があり、引き算を利用することで、手番の数字が1なら2、2なら1になるような最も短いコードで表現しています。

currentColor = 3 - currentColor;

以上で、ゲーム進行のためのメイン実装はすべて完了です。

残りは仕上げだけです。

あと一息、がんばりましょう!

 

パスボタンを実装しよう

🕛目安時間:30分

本章では、残りの要件である「ユーザーがパスボタンを押したら、相手の手番にする」機能を実装します。

前章までと比べると重要度は下がりますので、肩の力を抜いて取り組んでください。

まずはHTML要素を追加しましょう。

<body>
  <!-- 省略 -->
  <button id="pass">パスする</button>
</body>

 

次に、main.jsです。

//getReversibleStones関数のすぐ上に書きましょう
const passButton = document.getElementById("pass");
//window.onloadを以下のように編集しましょう
window.onload = () => {
  createSquares();

  passButton.addEventListener("click", () => {
    currentColor = 3 - currentColor;
  
    if (currentColor === 1) {
      currentTurnText.textContent = "黒";
    } else {
      currentTurnText.textContent = "白"
    }
  })
}

これでパスの機能が実装できました。

 

リファクタリングをしてみよう

前章までで作成したコードで、要件を満たす動作が実現できました。

しかし、JavaScriptコードの中に改善すべき点があります。

それは、「相手の手番に渡す」処理が、2回重複して登場してしまっていることです。

//下記の処理が2回登場している
currentColor = 3 - currentColor;
  
if (currentColor === 1) {
  currentTurnText.textContent = "黒";
} else {
  currentTurnText.textContent = "白";
}

重複した記述は、関数化することで使い回しできるようにすると、より管理しやすいコードになります。

このように、一度書いたコードを見直し、よりよくすることの前半をリファクタリングと呼びます。

 

それでは実際に、手番を渡す処理を関数化してみましょう。

//getReversibleStones関数のすぐ上に書きましょう
const changeTurn = () => {
  currentColor = 3 - currentColor;
  
  if (currentColor === 1) {
    currentTurnText.textContent = "黒";
  } else {
    currentTurnText.textContent = "白";
  }
}
//onClickSquare関数を編集しましょう
const onClickSquare = (index) => {
  //省略
  //重複処理部分(関数内最後のパート)を編集
  changeTurn();
}
//window.onload関数を編集しましょう
window.onload = () => {
  createSquares();

  passButton.addEventListener("click", changeTurn);
}

以上がリファクタリングの内容です。

重複していた部分がchangeTurn()の1行に置き換わり、だいぶすっきりしましたね。

開発現場では、動作することと同じくらい、コードの管理のしやすさが重視されます。

適切にリファクタリングできる能力は非常に重宝されていますので、常に意識するとよいですよ。

 

まとめ

ようやくすべての実装が完了です!

お疲れ様でした。

本記事の要点をまとめると、以下の通りです。

  • オセロゲームのルールを正しく把握し、要件を満たす設計をしよう
  • 場合分けを意識して処理フローを作成しよう
  • 処理フロー図の通りにコードに起こそう
  • うまくいかない時は、ブラウザ開発者ツールで検証をしよう
  • コードが動作するようになったら、リファクタリングをしよう

本記事では、やや長く複雑なコードを実装しました。

しかし上記のように設計とコードを丁寧にひもづけていくことで、整然とした実装が可能です。

難しく感じた方も多いと思いますが、本記事の内容を完璧に理解する頃には、あなたは立派な開発者レベルの能力を持っていると言えるでしょう。

 

最後に、作成したコードの全体を掲載します。

ぜひ復習やコードチェックに利用してください。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>オセロゲーム</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- 現在の手番を表示する -->
  <p>現在の手番は<span id="current-turn">黒</span>です</p>
  <!-- 盤 -->
  <div id="stage" class="stage"></div>
  <!-- 各マスを描画するためのテンプレート -->
  <div id="square-template" class="square">
    <div class="stone"></div>
  </div>
  <!-- パスボタン -->
  <button id="pass">パスする</button>
  <script src="main.js"></script>
</body>
</html>
*, *::before, *::after {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  background-color: #008000;
}

.stage {
  display: flex;
  flex-wrap: wrap;
  margin: 60px;
  width: 404px;
  height: 404px;
}

.square {
  position: relative;
  width: 50px;
  height: 50px;
  border: solid black;
  border-width: 0 4px 4px 0;
  cursor: pointer;;
}

.square:nth-child(-n + 8) {
  border-width: 4px 4px 4px 0;
  height: 54px;
}

.square:nth-child(8n + 1) {
  border-width: 0 4px 4px 4px;
  width: 54px;
}

.square:first-child {
  border-width: 4px;
  width: 54px;
  height: 54px;
}

.stone {
  position: absolute;
  top: 2px;
  bottom: 0;
  left: 2px;
  width: 42px;
  height: 42px;
  border-radius: 21px;
}

.stone[data-state="0"] {
  display: none;
}

.stone[data-state="1"] {
  background-color: black;
}

.stone[data-state="2"] {
  background-color: white;
}

#square-template {
  display: none;
} 
const stage = document.getElementById("stage");
const squareTemplate = document.getElementById("square-template");
const stoneStateList = [];
let currentColor = 1;
const currentTurnText = document.getElementById("current-turn");
const passButton = document.getElementById("pass");

const changeTurn = () => {
  currentColor = 3 - currentColor;
  
  if (currentColor === 1) {
    currentTurnText.textContent = "黒";
  } else {
    currentTurnText.textContent = "白";
  }
}

const getReversibleStones = (idx) => {
  //クリックしたマスから見て、各方向にマスがいくつあるかをあらかじめ計算する
  //squareNumsの定義はやや複雑なので、理解せずコピーアンドペーストでも構いません
  const squareNums = [
    7 - (idx % 8),
    Math.min(7 - (idx % 8), (56 + (idx % 8) - idx) / 8),
    (56 + (idx % 8) - idx) / 8,
    Math.min(idx % 8, (56 + (idx % 8) - idx) / 8),
    idx % 8,
    Math.min(idx % 8, (idx - (idx % 8)) / 8),
    (idx - (idx % 8)) / 8,
    Math.min(7 - (idx % 8), (idx - (idx % 8)) / 8),
  ];
  //for文ループの規則を定めるためのパラメータ定義
  const parameters = [1, 9, 8, 7, -1, -9, -8, -7];

  //ここから下のロジックはやや入念に読み込みましょう
  //ひっくり返せることが確定した石の情報を入れる配列
  let results = [];

  //8方向への走査のためのfor文
  for (let i = 0; i < 8; i++) {
    //ひっくり返せる可能性のある石の情報を入れる配列
    const box = [];
    //現在調べている方向にいくつマスがあるか
    const squareNum = squareNums[i];
    const param = parameters[i];
    //ひとつ隣の石の状態
    const nextStoneState = stoneStateList[idx + param];

    //フロー図の[2][3]:隣に石があるか 及び 隣の石が相手の色か -> どちらでもない場合は次のループへ
    if (nextStoneState === 0 || nextStoneState === currentColor) continue;
    //隣の石の番号を仮ボックスに格納
    box.push(idx + param);

    //フロー図[4][5]のループを実装
    for (let j = 0; j < squareNum - 1; j++) {
      const targetIdx = idx + param * 2 + param * j;
      const targetColor = stoneStateList[targetIdx];
      //フロー図の[4]:さらに隣に石があるか -> なければ次のループへ
      if (targetColor === 0) continue;
      //フロー図の[5]:さらに隣にある石が相手の色か
      if (targetColor === currentColor) {
        //自分の色なら仮ボックスの石がひっくり返せることが確定
        results = results.concat(box);
        break;
      } else {
        //相手の色なら仮ボックスにその石の番号を格納
        box.push(targetIdx);
      }
    }
  }
  //ひっくり返せると確定した石の番号を戻り値にする
  return results;
};

const onClickSquare = (index) => {
  //ひっくり返せる石の数を取得
  const reversibleStones = getReversibleStones(index);

  //他の石があるか、置いたときにひっくり返せる石がない場合は置けないメッセージを出す
  if (stoneStateList[index] !== 0 || !reversibleStones.length) {
    alert("ここには置けないよ!");
    return;
  }

  //自分の石を置く 
  stoneStateList[index] = currentColor;
  document
    .querySelector(`[data-index='${index}']`)
    .setAttribute("data-state", currentColor);

  //相手の石をひっくり返す = stoneStateListおよびHTML要素の状態を現在のターンの色に変更する
  reversibleStones.forEach((key) => {
    stoneStateList[key] = currentColor;
    document.querySelector(`[data-index='${key}']`).setAttribute("data-state", currentColor);
  });

  //もし盤面がいっぱいだったら、集計してゲームを終了する
  if (stoneStateList.every((state) => state !== 0)) {
    const blackStonesNum = stoneStateList.filter(state => state === 1).length;
    const whiteStonesNum = 64 - whiteStonesNum;

    let winnerText = "";
    if (blackStonesNum > whiteStonesNum) {
      winnerText = "黒の勝ちです!";
    } else if (blackStonesNum < whiteStonesNum) {
      winnerText = "白の勝ちです!";
    } else {
      winnerText = "引き分けです";
    }

    alert(`ゲーム終了です。白${whiteStonesNum}、黒${blackStonesNum}で、${winnerText}`)
  }

  //ゲーム続行なら相手のターンにする
  changeTurn();
}

const createSquares = () => {
  for (let i = 0; i < 64; i++) {
    const square = squareTemplate.cloneNode(true);
    square.removeAttribute("id");
    stage.appendChild(square);

    const stone = square.querySelector('.stone');

    let defaultState;
    //iの値によってデフォルトの石の状態を分岐する
    if (i == 27 || i == 36) {
      defaultState = 1;
    } else if (i == 28 || i == 35) {
      defaultState = 2;
    } else {
      defaultState = 0;
    }

    stone.setAttribute("data-state", defaultState);
    stone.setAttribute("data-index", i); //インデックス番号をHTML要素に保持させる
    stoneStateList.push(defaultState); //初期値を配列に格納

    square.addEventListener('click', () => {
      onClickSquare(i);
    });
  }
}

window.onload = () => {
  createSquares();
  passButton.addEventListener("click", changeTurn)
}

本記事は以上です。

 

大石ゆかり

たくさんコードを書いて疲れたけれど、解説が分かりやすくて良かったです!

田島悠介

ゆかりちゃんも分からないことがあったら質問してね!

大石ゆかり

分かりました。ありがとうございます!

 

JavaScriptを学習中の方へ

これで解説は終了です、お疲れさまでした。

  • つまずかず「効率的に」学びたい
  • 副業や転職後の「現場で使える」知識やスキルを身につけたい

プログラミングを学習していて、このように思ったことはありませんか?

テックアカデミーのフロントエンドコースでは、第一線で活躍する「プロのエンジニア」が教えているので、効率的に実践的なスキルを完全オンラインでしっかり習得できます。

合格率10%の選考を通過した、選ばれたエンジニアの手厚いサポートを受けながら、JavaScript・jQueryを使ったWebサービス開発を学べます。

まずは一度、無料体験で学習の悩みや今後のキャリアについて話してみて、「現役エンジニアから教わること」を実感してみてください。

時間がない方、深く知ってから体験してみたい方は、今スグ見られる説明動画から先に視聴することをおすすめします!