反復処理
プログラミングをしていて、同じ処理を繰り返し行いたい場合があります。
そんな時に利用すると便利なものをご紹介します。
ゲームを作る際には無限ループという方法を利用していることが多いです。
条件式や加算式、内部処理を間違えると無限ループとなり、ブラウザやPCがクラッシュすることがあるので、実行する際は注意してください。
for 文
まずは for 文の書き方(文法)です。
/*
for(初期化式; 条件式; 加算式) {
繰り返し行いたい処理
}
*/
for(let i = 0; i < 5; i = i + 1) {
console.log(i);
}
/*
for 文を使わないで書いた場合
let i = 0;
console.log(i);
i = i + 1;
console.log(i);
i = i + 1;
console.log(i);
i = i + 1;
console.log(i);
i = i + 1;
console.log(i);
*/
実行結果
0~4までが出力されました。
条件式(i < 5) は i が 5よりも小さい場合は繰り返すということになります。
for 文を使わずに書いた場合よりも読みやすくなったと思います。
TicTacToe
さて、あまり恩恵がわからなかったと思うので、少し発展的な内容にしてみます。
所謂マルバツゲームを作ってみました。
変数・分岐処理の部分においても、少し違う書き方をしています。
const area = document.getElementById('area'); // 盤面
let result = [0, 0, 0, 0, 0, 0, 0, 0, 0];
// result 配列の添え字が同じ手番なら勝ち
const end = [
[0,1,2],
[0,3,6],
[0,4,8],
[1,4,7],
[2,4,6],
[2,5,8],
[3,4,5],
[6,7,8],
];
let line; // 盤面の行
let turn = 0; // 手番
let winner = -1; // 勝者
// 3 * 3 の盤面なので 9回繰り返す
for(let i = 0; i < 9; i ++) {
// 1行に3個並べたいので、3回毎に block 要素を追加する
if(i % 3 === 0) {
line = document.createElement('div');
area.appendChild(line);
}
// ボタンの生成
const button = document.createElement('button');
// ボタンをクリックした時に動作するイベント
button.addEventListener('click', e => {
// すでにクリックされているか、勝敗が決まっている場合は処理をスキップする
if(result[i] !== 0 || winner !== -1) return;
// 手番によって ○ か × を判断する
if(turn % 2 === 0) {
button.innerHTML = '○';
result[i] = 1
} else {
button.innerHTML = '×';
result[i] = -1;
}
// 勝敗チェック
let cnt = 0;
for(let j = 0; j < end.length; j ++) {
for(let k = 0; k < end[j].length; k ++) {
// result 配列がまだ埋まっていない場合は次の列へ
if(result[end[j][k]] === 0) {
cnt = 0;
continue;
}
cnt += result[end[j][k]];
}
// ○ か × が 3個並んでいたら勝者を決める
if(cnt === 3 || cnt === -3) {
winner = turn % 2;
break;
}
cnt = 0;
}
if(winner === 0) {
alert('○の勝ち');
} else if(winner === 1){
alert('×の勝ち');
}
// 次の手番へ
turn ++;
});
// ボタンを行に追加
line.appendChild(button);
}
<!DOCTYPE html>
<html lang="ja" dir="ltr">
<head>
<meta charset="utf-8">
<title>TicTacToe</title>
</head>
<body>
<style type="text/css">
.wrap {
width: 100%;
text-align: center;
}
button {
width: 30px;
height: 30px;
font-size: 20px;
margin: 5px;
vertical-align: bottom;
}
</style>
<div class="wrap">
<div id="area">
</div>
</div>
<script src="TicTacToe.js" charset="utf-8"></script>
</body>
</html>
今回はhtmlファイルも用意しました。こちらで遊べます。
解説
変数・分岐処理では出てこなかったものをいくつか取り入れてみました。
というのも、プログラミングをしているとよく見ますが、最初は少し分かりにくいと思ったので出さなかった部分があります。
また、1時間くらいで作るものを考え、コーディングをしているので、バグが潜んでいるかもしれませんし、最適ではないかもしれません。
ご了承ください。
1行目は html の盤面を取得しています。
div という要素についている id が area のものを探す、という方法をとっています。
ちなみに id が間違っていると、area には null が代入されます。
詳しい使い方についてはいつも通り本家でどうぞ。
2行目では配列を宣言しています。
配列というのは、複数の変数を一つにまとめたようなものだと思ってください。
厳密には違いますし、内部構造を少し説明した方が良いのですが、混乱するので別の章でまとめます。
4行目は勝利判定に使用する二次元配列です。
配列の中に配列を格納することで、テーブルのようにすることができます。
要素番号0 | 要素番号1 | 要素番号2 | |
---|---|---|---|
要素番号0 | 0 | 1 | 2 |
要素番号1 | 0 | 3 | 6 |
要素番号2 | 0 | 4 | 8 |
要素番号3 | 1 | 4 | 7 |
.
.
.
各要素への参照の仕方は 変数名[要素番号][要素番号] です。
下記だと、 array2[0][0] とすると 'test-0-0' を参照できます。
分かりにくい場合は、 array2[0] は ['test0-0', 'test0-1'] で、その中の0番目 'test-0-0' というように、分解して考えても良いかもしれません。
ちなみに二次元以上もできます。
3Dなどを扱う場合はよく見かけます。
// 変数名 = [要素, 要素, 要素 …]; と書く
// 要素の数を要素数といい、0番目(一番左)にあるものを要素番号0(添え字が0)という
let array1 = ['test0', 'test1', 'test2'];
console.log(array1[0]);
// コンソールに test0 と表示される
console.log(array1[1]);
// コンソールに test1 と表示される
// 存在しない添え字を指定すると…
console.log(array1[3]);
// コンソールに undefined と表示される
// undefined は未定義という意味
// 配列の要素の数を教えてくれる
console.log(array1.length);
// 3
// 配列の中に配列を入れることもできる
let array2 = [
['test0-0', 'test0-1'],
['test1-0', 'test1-1'],
['test2-0', 'test2-1'],
];
// 使い方はそんなに変わらない
console.log(array2);
console.log(array2[0][0]);
// test0-0
console.log(array2[1][0]);
// test1-0
console.log(array2[2][1]);
// test1-1
console.log(array2.length);
// 3
console.log(array2[0].length);
// 2
14行目は盤面を生成する際、改行するために <br> を使用するとうまくいかなかったので、 div 要素を採用するための変数です。
15行目は手番、16行目は勝敗が決まった場合、勝者の手番を格納する変数になります。
18行目からはやっと for 文です。
内容としては、
1.盤面(ボタン)の生成
2.*クリックした時のイベントの設定
3.ボタンを画面上に描画
になります。
*イベントの設定は
1.ボタンがクリックされた時に手番のマーク(○か×)をボタンに表示
2.勝敗チェック
3.手番を渡す
という処理です。
今までの章で紹介できていない部分を掻い摘んで説明して行きます。
document については、次回の関数や次々回のオブジェクトの章で説明します。
定数
4行目、 const というキーワードが出てきました。
const を使用すると定数を宣言できます。
定数は一度代入すると、値の変更ができなくなります。
また、必ず宣言時に初期化しなくてはなりません。
どのような利点があるかというと、間違えて再代入することができなくなるため、ミスやバグの発見をしやすくなります。
const start = 0;
start = 1; // エラー 読み取り専用だよ!って怒られる
const end; // エラー 初期化しないとダメだよ!って怒られる
インクリメント
18行目、加算式にある i ++ についてです。
これは後置インクリメントといいます。
i -- というものもあり、これは後置デクリメントです。
let i = 0;
i ++;
// i = i + 1 と同義
// また i += 1 も同義
console.log(i); // 1;
let d = 1;
d --;
// d = d - 1 と同義
// また d -= 1 も同義
console.log(d); // 0;
// それを踏まえて
let start = 0;
start += 11;
// start = start + 11; となる
剰余
20行目では、 if 文の中に % という演算子が出てきました。
これは剰余を求める演算子になります。
剰余とは平たく言えば、数同士で割った余りを求める演算子です。
ゲームだとキャラクターの移動処理によく出てきますね。
console.log(0 % 4); // 0
console.log(1 % 4); // 1
console.log(2 % 4); // 2
console.log(3 % 4); // 3
今回は3回毎に div の要素を追加する際に使用しています。
i が 0 の時、 div を生成して html に追加。
1, 2 の時は余りが 0 にならないため実行されない。
i が 3 の時、 div を生成して html に追加。
...以下略
このように剰余を使用すると任意のタイミングで if 文の中を実行することができます。
ちなみに剰余を利用しない場合は、
for(let i = 0; i < 9; i ++) {
if(i === 0) {
// i が 0 の時実行される
} else if(i === 3) {
// i が 3 の時実行される
} else if(i === 6) {
// i が 6 の時実行される
}
// もしくは
if(i === 0 || i === 3 || i === 6) {
// i が 0,3,6 の時実行される
}
}
となります。
剰余を利用した時の利点は、盤面を増やしたい場合(例えば5 * 5)に、for 文の条件式(18行目)を i < 25 とし、if(i % 5 === 0) (20行目)とするだけで済むということです。
勝敗の部分も変えなくてはなりませんが、修正する箇所が減るので、管理しやすくなります。
さらっと else if という文も出てきましたが、複数の条件で実行することができるという文になります。
論理演算子
29行目の if 文で || というものが出てきました。
これは論理演算子といい、数学で言うところの OR に近い意味があります。
複数の条件を指定したい場合に利用されます。
今回ですと、
ボタンに何か描画がされている時・もしくは勝敗が決まっている時、処理を抜ける(return 文)
という使い方をしています。
|| は論理和(OR)で、どちらか片方・もしくは両方の条件を満たしている場合、
&& は論理積(AND)で、両方の条件を満たしている場合、に実行されます。
ちなみに || を利用して最初の条件式を評価し、turu を返す場合、2個目の条件式を評価しない仕様があります。
たまにハマることがあるので、記憶の片隅にでも覚えておくと便利です。
let text = 'text';
let num = 3;
if(text === 'text' || num === 6) {
// 実行される
}
if(text === 'text' && num === 6) {
// 実行されない
}
if(text === 'text' && num === 3) {
// 実行される
}
if(text === 'text' || aaa) {
// 実行される
}
// if(aaa) {
// console.log("TEST")
// }
// 16-18行目のコメントアウトを外して実行すると、ReferenceError: Can't find variable: aaa となります。
// aaa なんて変数見つからないよ!と怒られます。
配列
続いて40行目です。
end.length というものが出てきました。
解説の初め、配列の宣言時にも説明した通り、配列は単なる変数ではないことがわかります。
詳しいことはオブジェクトの章で説明しますので、配列.length で要素の数を知ることができるということだけお伝えしておきます。
continue と break
45行目に continue という文が現れました。
これはこの文に達した時に、 for 文の初めに戻ること意味します。
今回ですと、
調べたボタンがまだ押されていない場合は、for 文の初めに戻る
という処理をしています。
「初めに戻る」という表現ですが、意味としては、 for 文の加算式を実行し、条件式を評価する
ということになります。
つまり今回は二重ループ(for 文の中に for 文)なので、内側の for 文の加算式(k ++)を実行し、条件式(k < end[j].length)を評価しています。
52行目では break という文が現れます。
break に達した時、for 文から抜けます。
break も内側の for 文のみ抜けるので、今回は問題になりませんが、二重ループの内側で使用した場合は、外のループはそのまま実行されます。
ここからは余談というか、ロジックの説明になるので、興味がなければ+α進んでください。
ゲームロジック
○×ゲームは同じマークが3個並んだら勝敗が決まります。
つまり、
0 1 2
3 4 5
6 7 8
という番号が振られているボタンのうち、縦横斜めのいずれかが同じマークが並んでいれば良いということになります。
冒頭 result 配列は上記の並んでいるボタンを表していますので、33行目と36行目では押下されたボタンに1と-1を格納しています。
end という二次元配列では、同じマークが並ぶ可能性がある要素番号を列挙しています。
0 1 2 であれば、1段目が横に並んでいるということ、
0 4 8 であれば、左上から右下に斜めに並んでいるということになります。
40行目、 end 配列の要素(1列に並ぶライン)の数だけ繰り返す for 文、
41行目、どの列(縦横斜)が並んでいるかを調べる for 文となります。
43行目、33行目と36行目でボタンに1と-1を格納しているため、result の要素が0の時、そのボタンは押されていないということになります。
なので、押されてないボタンが存在する時点で、その列で勝敗は決まらないことが分かるので、次の列を調べる処理をしています。
47行目では、マークの種類を加算しています。
◯なら +1、×なら -1を足しています。
こうすることで、3個の同じマークが並んだ場合、3、もしくは-3になります。
50行目、3個並んでいる場合には勝敗を決めます。
置いた瞬間に勝ち負けが決まるので、現在の手番の剰余で、どちらの(勝利した)手番か分かります。
52行目、勝敗が決まったので、勝敗チェックのループを抜けます。
56行目、勝敗の決定処理をしています。
実行する文が1行の場合、 {} を省略することができます。
let turn = 10;
if(turn % 2 === 0) console.log("2で割り切れます。");
else console.log("2で割り切れません。");
console.log("turn が奇数でも偶数でも出力されます。");
可読性と保守性に関しては条件文の長さや、処理によるので一概には言えませんが、慣れないうちは {} ありの方が良いかと思います。
59行目、次の手番へするために、ターンを加算しています。
65行目、ボタンにイベントを登録したので、ボタンを行に追加する処理です。
+α
さて、今回のマルバツゲームですが、実は少し不便です。
というのも、勝負がつかなかった場合、引き分けが分かりません。
また、勝負がついた(引き分けを含む)後、再戦するためにはブラウザのリロードするしかありません。
遊ぶ人のことを考えると、リロードは面倒なので初期化(再戦)処理が必要になります。
ぜひ改造してみてください。
ちなみに、こちらは引き分け処理と再戦処理と勝敗記録を追加したものになります。
答え代わりにどうぞ。ただ、書き方は一つではないので、あくまでも一例として見てくださいね。