画面遷移なしでメールフォームから送信する

やりたいこと
メールフォームの送信を画面遷移なしで行う。JavaScript(Ajax)を使う。

前回、PHPMailerを使ってメール送信できるところまで作った。index.html内のform要素からsubmit.phpにPOSTすることでメール送信を行うが、送信ボタンを押すとsubmit.phpに画面遷移するので、画面遷移なしでこれを行いたい。

ここで、ファイルの構成は

  • index.html(送信フォーム)
  • mail(フォルダ)
    • PHPMailer(ライブラリ一式)
    • mail.php
    • submit.php
    • js_submit.php
index.html
<form action="./mail/submit.php" method="post" id="mail-form">

<dl>
<dt><label for="name">お名前(必須)</label></dt>
<dd><input type="text" name="name" size="25" id="name" required></dd>

<dt><label for="from">メールアドレス</label></dt>
<dd><input type="text" name="from" id="from" size="25"></dd>

<dt><label for="body">本文(必須)</label></dt>
<dd><textarea name="body" id="body" cols="25" rows="5" required placeholder="ご意見・ご感想"></textarea></dd>
</dl>

<div><input type="submit" value="送信"></div>

</form>

送信ボタンを押すと、submit.phpに対して、投稿者の名前(name), メールアドレス(from), 本文(body)を送る。これらはPHPの$_POST変数に連想配列として格納される。

submit.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ご意見・ご感想</title>
</head>
<body>

<?php
include 'mail.php';
$message = phpMailerSend($to, $subject, $body, $from_address, $from_name);
?>

<p>
<?php
echo $message;
?>
</p>

<ul>
  <li><a href="../index.html">トップへ戻る</a></li>
</ul>

</body>
</html>

送信後に表示する画面。JavaScript無効の環境ではこのファイルからメール送信が行われ、トップページに戻るリンクを表示する。

mail.php
<?php
mb_internal_encoding("UTF-8");
require 'PHPMailer/PHPMailerAutoload.php';


$to = 'sample@example.com'; // 自分のメールアドレス

if ($_POST['subject'] != ''){
  $subject = htmlspecialchars($_POST['subject']);
}else {
  $subject = '無題';
}

$body = htmlspecialchars($_POST['body']);

if($_POST['name'] !=''){
  $from_name = htmlspecialchars($_POST['name']);
}else {
  $from_name = '匿名さん';
}

if($_POST['from'] != ''){
  $from_address = htmlspecialchars($_POST['from']);
}else {
  $from_address = '';
  $body .= ' posted by ';
  $body .= $from_name;
}


function phpMailerSend($to, $subject, $body, $from_address, $from_name){

  $mailer = new PHPMailer();

  $mailer -> CharSet = "iso-2022-jp";
  $mailer -> Encoding = "7bit";

  $mailer->AddAddress($to);
  $mailer->From = $from_address;
  $mailer->FromName = mb_encode_mimeheader($from_name);
  $mailer->Subject = mb_encode_mimeheader($subject);
  $mailer->Body = mb_convert_encoding($body, 'JIS', 'UTF-8');

  if($mailer->Send()){
    $message = '送信しました!';
  }else {
    $message = '送信に失敗しました。ErrorInfo: ';
    $message .= $mailer->ErrorInfo;
  }

  return $message;
}
?>

メールを送信するための関数phpMailerSendの定義と、関数に渡すための引数を用意する。今回、送信フォームから送られてくるのは投稿者の名前($_POST[‘name’])、メールアドレス($_POST[‘from’])、本文($_POST[‘body’])の3つだけだが、件名も用意しておいた(これで送信フォームに後から件名用のフィールドを足すことも出来る)。

ここまででメール送信フォームの準備が出来た。JavaScript無効な環境でも問題なく動作する。だだ、送信ボタンを押すと./mail/submit.phpに画面遷移するのが個人的に気に入らないので、JavaScriptを使って画面遷移なしで各フィールドの値をPOSTできるようにする。

  • JavaScriptで、フォームのデフォルトの振る舞いを無効化する。
  • PHPへのPOST要求はフォーム送信からではなく、JavaScriptから行う。
  • JavaScriptでのPOST要求にはAjaxオブジェクトのsend()メソッドを使う。

Ajaxオブジェクト(XML HTTP Requestオブジェクト)の作成は、古いIEを考慮した以下のコードで行う。

function getXMLHttpRequestObject(){
  var ajax = null;

  if(window.XMLHttpRequest){
    ajax = new XMLHttpRequest();
  }else if(window.ActiveXObject){// 古いIE
    ajax = new ActiveXObject('MSXML2.XMLHTTP.3.0');
  }

  return ajax;
}

AjaxオブジェクトのreadyStateが変化したときに実行される関数を作る。Ajaxオブジェクトはイベントのターゲットから取得する。

function handleAjaxResponse(e){
  'use strict';
  if(typeof e == 'undefined') e = window.event;
  var ajax = e.target || e.srcElement;

  if(ajax.readyState == 4){
    if((ajax.status >= 200 && ajax.status < 300) || (ajax.status == 304)) {
      // ステータスOKの場合、
      alert('送信しました!');// 仮のメッセージ。後で書き換える

    } else {
      // ステータスエラーの場合、通常のメール送信(post)を行う
      document.getElementById('mail-form').submit();
    }
    ajax = null;
  }
}

windowがロードしたときに無名関数でAjaxオブジェクトを作成しておき、そのreadyStateが変化したときに、上記のhandleAjaxResponse関数が実行されるようにする。

この関数の中で、送信フォームから送信(onsubmit)が行われたときに実行される無名関数を作っておく。こうすることで、Ajaxオブジェクトをグローバルなスコープに書かなくても、関数内からアクセスできる。

window.onload = function(){
  'use strict';
  var ajax = getXMLHttpRequestObject();//XMLHttpRequestインスタンスを作る
  ajax.onreadystatechange = handleAjaxResponse;

  document.getElementById('mail-form').onsubmit = function(){
    // js_submit.phpに送信するデータを作成する
    var fields = ['name', 'from', 'body'];
    var data = [];//空の配列
    for(var i = 0, count = fields.length; i < count; i++){
      data.push(encodeURIComponent(fields[i]) + '=' + encodeURIComponent(document.getElementById(fields[i]).value));
    }
    
    ajax.open('POST', 'js_submit.php', true);
    ajax.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    ajax.send(data.join('&'));
    
    return false;// フォームからの実際の送信をとめる
  
  }; // onsubmit無名関数の終わり
}; // onload無名関数の終わり

配列のjoin()メソッドにより、配列の各要素の値を&でつないだStringを与えている。これで、.send()メソッドによってjs_submit.phpに送られるのは、たとえば「name=なまえ&from=tokumei@mail.com&body=本文」がURLエンコードされた値となる。

js_submit.php
<?php
mb_internal_encoding("UTF-8");

include 'mail.php';

$message = phpMailerSend($to, $subject, $body, $from_address, $from_name);
echo $message;

?>

仮のメッセージはajax.responseTextに書き換えた。phpMailerSendが返すメッセージ($message)を表示する。

    if((ajax.status >= 200 && ajax.status < 300) || (ajax.status == 304)) {
      // ステータスOKの場合、
      alert(ajax.responseText);

入力されたメールアドレスの形式が正しくない場合、PHPMailerのSend()メソッドで送信失敗するので、メールアドレスのチェックを行う必要がある。これは次回考えよう。

メールフォームを作る

やりたいこと
さくらインターネット上のWebサイトにメールフォームを設置し、メールを送信できるようにする。PHPMailerというライブラリを使う。

「Clone or download」というボタンから、ZIPファイルをダウンロードした。解凍して出来たフォルダをPHPMailerとリネームしてから、さくらインターネットのサーバーの /home/アカウント名/www/mail/ に アップロードした。

ここで、ファイルの構成は

  • index.html
  • mail
    • PHPMailer(ライブラリ一式)
    • mail.php
    • submit.php

メールフォームを設置するHTMLファイル(index.html)にform要素を追加した。送信者の名前とメールアドレス、本文のみとした。件名は省略。宛先アドレスは自分宛にする。メールフォームのスタイルはCSSで適当に調整すればよい。

index.html
<form action="./mail/mailsender.php" method="post" id="mail-form">

<dl>
<dt><label for="name">お名前(必須)</label></dt>
<dd><input type="text" name="name" size="25" id="name" required></dd>

<dt><label for="from">メールアドレス</label></dt>
<dd><input type="text" name="from" id="from" size="25"></dd>

<dt><label for="body">本文(必須)</label></dt>
<dd><textarea name="body" id="body" cols="25" rows="5" required placeholder="ご意見・ご感想"></textarea></dd>
</dl>

<div><input type="submit" value="送信"></div>

</form>

submit.phpからメールを送信する。form要素からPHPにpostすると、input要素やtextarea要素の内容がそれらのname属性の値とセットで、$_POSTという変数に連想配列として格納される。submit.phpで$_POSTの内容を取り出し、メール送信用の関数phpMailerSend()に引き渡す。phpMailerSend()はmail.phpで定義する。

submit.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>ご意見・ご感想</title>
</head>
<body>

<?php

$to = '宛先メールアドレス';

if ($_POST['subject'] != ''){
  $subject = htmlspecialchars($_POST['subject']);
}else {
  $subject = 'メールフォームから投稿がありました';
}

$body = htmlspecialchars($_POST['body']);

if($_POST['name'] !=''){
  $from_name = htmlspecialchars($_POST['name']);
}else {
  $from_name = '匿名さん';
}

if($_POST['from'] != ''){
  $from_address = htmlspecialchars($_POST['from']);
}else {
  $from_address = '';
  $body .= ' posted by ';
  $body .= $from_name;
}

include 'mail.php';

$message = phpMailerSend($to, $subject, $body, $from_address, $from_name);
?>

<p>
<?php
echo $message;
?>
</p>

<ul>
  <li><a href="WebサイトのURL">トップへ戻る</a></li>
</ul>

</body>
</html>

mail.phpでPHPMailerを使ってメール送信用の関数を定義する。

インスタンスのプロパティやメソッドを使うとき、JavaScriptやPythonでは.(ドット)記法だったが、PHPでは->という記号を使うらしい。

  • $インスタンス名->プロパティ
  • $インスタンス名->メソッド()
mail.php
<?php
  mb_internal_encoding("UTF-8");
  require 'PHPMailer/PHPMailerAutoload.php';

  function phpMailerSend($to, $subject, $body, $from_address, $from_name){

    $mailer = new PHPMailer();

    $mailer-> CharSet = "iso-2022-jp";
    $mailer-> Encoding = "7bit";

    $mailer->AddAddress($to);
    $mailer->From = $from_address;
    $mailer->FromName = mb_encode_mimeheader($from_name);
    $mailer->Subject = mb_encode_mimeheader($subject);
    $mailer->Body = mb_convert_encoding($body, 'JIS', 'UTF-8');

    if($mailer->Send()){
      $message = '送信しました!';
    }else {
      $message = '送信に失敗しました。ErrorInfo: ';
      $message .= $mailer->ErrorInfo;
    }

    return $message;
  }
?>

phpMailerSend関数は、メール送信を行うとともに、メッセージを返す。送信に失敗した場合はエラー情報を表示する。FromNameとSubjectはmb_encode_mimeheaderを入れないと文字化けした。

14. 4つずつ落とす

本書では13章の内容だが、第14回目に分割して書く。前回までで、四角を3つずつ扱うことができるようになった。今回はこれを4つずつできるようにする。

まず、drawBlock, eraseBlock, isStackedBlockを変更する。

function drawBlock(y, x, r, s, color){
	
	var positions = otherYXs(y, x, r, s);
	
	drawRect(y, x, color);
	drawRect(positions[0], positions[1], color);
	drawRect(positions[2], positions[3], color);
	drawRect(positions[4], positions[5], color);
}

drawRectを四角4つ分書いているので、ループにまとめてしまおう。

function drawBlock(y, x, r, s, color){

	var deltas = offsets[s][r];
	
	drawRect(y, x, color);
	
	for(var i = 0; i < 3; i++){
		drawRect(y + deltas[2 * i], x + deltas[(2 * i)+1], color);
	}
}

otherYXs(y, x, r, s)の代わりにoffsets[s][r]を使った。offsetsは前回作ったブロックの位置を覚えておく多次元配列である。これは形と回転数を与えると、オフセット[y1, x1, y2, x2, y3, x3]を表す配列を返すことを想定している。たとえば[0, -1, 0, 1, 0, 2]となる。

drawRect(y, x, color)だけループの外に出したが、drawRect(y + 0, x + 0, color)と考えればループ内に書くことも出来る。そのほうがすっきりするが、deltas[0]とdeltas[1]はともに0とする必要がある。offsets[s][r]が[0, 0, y1, x1, y2, x2, y3, x3]を返すようにすればいいが、そのほうがよさそうならそうしよう。とりあえずこのまま進める。

eraseBlockは同じように書き換えられるので、isStackedBlockを考える。単純に4つめの四角の判定を追加するならこうなる。

function isStackedBlock(y, x, r, s){

	var positions = otherYXs(y, x, r, s);
	
	var isStackedA = isStacked(y, x);
	var isStackedB = isStacked(positions[0], positions[1]);
	var isStackedC = isStacked(positions[2], positions[3]);
	var isStackedD = isStacked(positions[4], positions[5]);
	
	return (isStackedA||isStackedB||isStackedC||isStackedD);
}

isStackedBlockは4つの四角の位置を見て、そのどれかひとつでも既に四角のある位置と重なっているならtrueを返す。
論理和をとる処理をループさせれば、どこかでtrueになれば全体としてtrueとなる。

function isStackedBlock(y, x, r, s){

	var deltas = offsets[s][r];
	var isStackedAny = isStacked(y, x);
	
	for(var i = 0; i < 3; i++){
		isStackedAny = isStackedAny || isStacked(y + deltas[(2 * i)], x + deltas[(2 * i)+1])
	}
	
	return isStackedAny;
}

ではoffsetsの中身を考えよう。四角が3つのときは、中心の四角以外の2つの四角の相対位置を表す4つの値[y1, x1, y2, x2]を持っていた。

var offsets =[[[ 0, 1, 0,-1],
        [ 1, 0,-1, 0],
        [ 0,-1, 0, 1],
        [-1, 0, 1, 0]],
       [[ 0, 1, 1, 0],
        [ 1, 0, 0,-1],
        [ 0,-1,-1, 0],
        [-1, 0, 0, 1]]]

四角が4つでは、[y3, x3]を追加するので、6つずつの値を持つことになる。さて、前回はこの配列の中身は表から書き起こしたので、同じように表を作ってみよう。

四角が4つになると、考えられるブロックの形は以下の7種類になる。左からs = 0, 1, 2, 3, 4, 5, 6に対応させる。

     □    □    □ □■  ■□  ■□
□■□□ □■□ □■□ □■□  □□ □□ □□

(y, x)を与えられる四角を■で、■の位置と回転数から位置が決まる残りの3つの四角を□で表した。

四角が3つのときは種類sは0,1の2種類、回転数rは0~3の4種類だったので、種類と回転数から決まるパターンは8つだった。

表1. 四角3つのときの配置パターン
s r y1 x1 y2 x2
0 0 0 1 0 -1
0 1 1 0 -1 0
0 2 0 -1 0 1
0 3 -1 0 1 0
1 0 0 1 1 0
1 1 1 0 0 -1
1 2 0 -1 -1 0
1 3 -1 0 0 1

四角が4つになると、(y3, x3)の列とs= 2~6の行が追加されて、表はこういう風になる。

表2. 四角4つのときの配置パターン
s r y1 x1 y2 x2 y3 x3
0 0
0 1
0 2
0 3
1 0
1 1
1 2
1 3
2 0
2 1
2 2
2 3
0
6 0
6 1
6 2
6 3

この表を埋めて配列offsetsを作れば完成なのだが、7種類×回転数4つの28パターンのそれぞれにつき四角の位置が6つ必要になる。とてつもなく面倒だ。

回転を計算で求めることはできないか。点(y1, x1)を原点の周りに90度回転した座標を計算で求めることができれば、必要なパターンは7種類で済む。

表3. 四角4つのときの配置パターン(r=0)
s r y1 x1 y2 x2 y3 x3
□■□□
0 0 0 -1 0 1 0 2
□
□■□
1 0 -1 -1 0 -1 0 1
 □
□■□ 
2 0 0 -1 -1 0 0 1
  □
□■□  
3 0 0 -1 -1 1 0 1
□■
 □□
4 0 0 -1 1 0 1 1
■□
□□
5 0 0 1 1 0 1 1
 ■□
□□
6 0 0 1 1 -1 1 0

高校の数学を思い出すと、点A(x, y)を原点の周りにθ回転させたときの座標A’は(xcosθ-ysinθ,xsinθ+ycosθ)なので、90度回転ではcos90°= 0, sin90°= 1だからA'(-y, x)となる。点(x, y)を与えると(-y, x)を返す関数と考えれば

function rotate([x, y]) {
	return [-y, x];
}

たとえばs = 0, r = 0の[0, -1, 0, 1, 0, 2]からs = 0, r = 1の座標を求めるには、それぞれの(y, x)に対してrotateを使えばいいので、rotate([x, y])を以下のように書き換えて

function rotate([x1, y1, x2, y2, x3, y3]) {
	return [-y1, x1, -y2, x2, -y3, x3];
}

それではr = 0の座標[0, -1, 0, 1, 0, 2]からr = 1,2,3の座標を順に作っていく。shapesという配列を作り、[0, -1, 0, 1, 0, 2]を回転させて出来る配列を入れていく。

var shapes = [];
var positions = [0, -1, 0, 1, 0, 2];// s=0,  r=0

for(var r = 0; r < 4; r++){
	positions = rotate(positions);
	shapes.push(positions);
}

ほかの6つの初期位置に対しても同様にする。shapesはr = 0~3に対応する4つの配列を要素に持つ二次元配列で、もともとほしい配列offsetsは、sの値それぞれに対応するshapesを要素として持つことになるので

var offsets = [];
var shapes = [];
var positions = [];

// s=0, r=0
shapes = [];
positions = [0, -1, 0, 1, 0, 2];

for(var r = 0; r < 4; r++){
	positions = rotate(positions);
	shapes.push(positions);
}

offsets.push(shapes);

// s=1,  r=0
shapes = [];
positions = [-1, -1, 0, -1, 0, 1];

for(var r = 0; r < 4; r++){
	positions = rotate(positions);
	shapes.push(positions);
}

offsets.push(shapes);

// s=2,  r=0
shapes = [];
positions = 以下略

これはもちろんループを使って書けばいいので

var offsets = [];

var positions = [
 [ 0,-1, 0, 1, 0, 2],// s= 0, r=0
 [-1,-1, 0,-1, 0, 1],// s= 1, r=0
 [ 0,-1,-1, 0, 0, 1],// s= 2, r=0
 [ 0,-1,-1, 1, 0, 1],// s= 3, r=0
 [ 0,-1, 1, 0, 1, 1],// s= 4, r=0
 [ 0, 1, 1, 0, 1, 1],// s= 5, r=0
 [ 0, 1, 1,-1, 1, 0] // s= 6, r=0
 ];

for(var s = 0; s < 7; s++){

	var shapes = [];

	for(var r = 0; r < 4; r++){
		positions[s] = rotate(positions[s]);
		shapes.push(positions[s]);
	}
	
	offsets.push(shapes);
	
}

以上で配列offsetsは表2のような配列となっているはずだ。otherYXs(y, x, r, s)は不要になったので削除する。

dropRectの中の以下の部分も書き換えよう。

// 横5, 縦1の位置に、
X = 5; 
Y = 1;
R = 0;
S = 1 - S; // 棒とLが交互

resetという関数にして、外に出す。

function reset(){
	X = 5;
	Y = 2;
	R = 0;
	S = S + 1;
	if(S > 6) S = 0;
}

これで、一応は完成となる。

以下、おまけ。handleKeyUpの中の押されたキーを判別するswitchの分岐に、Enterキーを押したときを追加してみた。

switch(key) {
	case 39://右
		deltaX = 1;
		break;
	case 37://左
		deltaX = -1;
		break;
	case 40:// 下
		deltaY = 1;
		break;
	case 38://上(回転)
		deltaR = 1;
		break;
		
	case 13:// Enter(種類変更)
		reset();
		break;
}

rotateが返す値を書き換えることで、回転方向を逆にすることも出来る。

function rotate([x1, y1, x2, y2, x3, y3]) {
	// return [-y1, x1, -y2, x2, -y3, x3];
	   return [y1, -x1, y2, -x2, y3, -x3]; // 逆回転
}

drawBlockの、ループ内のdrawRectだけcolor引数を無くせば、中心の四角だけが赤色になる。こういった変更が容易なので、drawRect(y, x, color)はループ内に書かずに、このままでよしとしよう。

function drawBlock(y, x, r, s, color){

	var deltas = offsets[s][r];
	
	drawRect(y, x, color);
	
	for(var i = 0; i < 3; i++){
		drawRect(y + deltas[2 * i], x + deltas[(2 * i)+1]);
	}
}

13. 3つずつ落とす

ここまでで、2つずつの四角が落ちてきて、キーボードで操作でき、回転もできて、1列積もると消えるところまで作った。次は四角を1つ増やし、3つの四角からなるブロックが落ちてくるようにしよう。

四角が3つになると何が変わるか。四角が2つのときは、ブロックの形は1種類だけだった。四角が3つになると、ブロックの形は3つが一直線に並んだ棒状のものと、折れ曲がったL字型のものの2種類になる。

2つ ■□

3つ □■□ □
       ■□

四角が2つのときは片方の四角の位置(y, x)と回転数(r)が決まれば、もう一方の四角の位置も確定していた。四角が3つの場合はどうだろうか。

棒状のものであれば、中心の四角の位置と回転数から他の2つの四角の位置も決まりそうだ。まず、このケースだけを考えて進めよう。

drawBlockを書き換える。3つめの四角を描く処理を追加するだけだ。eraseBlockも同じように書き換える。

function drawBlock(y, x, r, color){
	drawRect(y, x, color);
	switch(r){
		case 0:// 0度-横並び
			drawRect(y, x+1, color);
			drawRect(y, x-1, color);
			break;
		case 1://90度-縦並び
			drawRect(y+1, x, color);
			drawRect(y-1, x, color);
			break;
		case 2://180度-横並び
			drawRect(y, x-1, color);
			drawRect(y, x+1, color);
			break;
		case 3://270度-縦並び 
			drawRect(y-1, x, color);
			drawRect(y+1, x, color);
			break;
	}
}

isStackedBlockも同じように書き換える。

function isStackedBlock(y, x, r){
	var isStackedA = isStacked(y, x);
	var isStackedB = false;
	var isStackedC = false;
	
	switch(r){
		case 0:// 0度-横並び
			isStackedB = isStacked(y, x+1);
			isStackedC = isStacked(y, x-1);
			break;
		case 1://90度-縦並び
			isStackedB = isStacked(y+1, x);
			isStackedC = isStacked(y-1, x);
			break;
		case 2://180度-横並び
			isStackedB = isStacked(y, x-1);
			isStackedC = isStacked(y, x+1);
			break;
		case 3://270度-縦並び 
			isStackedB = isStacked(y-1, x);
			isStackedC = isStacked(y+1, x);
			break;
	}
	
	return (isStackedA||isStackedB||isStackedC);
}

これで、3つの四角が一列に並んだケースでは上手くいった。

しかし、ここで同じようなことを何回も書いているのが嫌な感じである。たとえば、(y, x, 0)のときには、drawRectとeraseRectとisStackedで(y, x+1), (y, x-1)を書いている。3箇所で同じことを書いているので、これは共通化しよう。

中心の四角の位置と回転数(y, x, r)を与えると、残りの2つの四角の位置(y1, x1, y2, x2)を返す関数を定義する。

function otherYXs(y, x, r){
	var positions = [];
	
	switch(r){
		case 0:// 0度-横並び
			positions = [y, x+1, y, x-1];
			break;
		case 1://90度-縦並び
			positions = [y+1, x, y-1, x];
			break;
		case 2://180度-横並び
			positions = [y, x-1, y, x+1];
			break;
		case 3://270度-縦並び 
			positions = [y-1, x, y+1, x];
			break;
	}
	return positions;
}

この関数を使えば、drawBlockはこのように簡単に書ける。

function drawBlock(y, x, r, color){
	
	var positions = otherYXs(y, x, r);
	
	drawRect(y, x, color);
	drawRect(positions[0], positions[1], color);
	drawRect(positions[2], positions[3], color);
}

eraseBlockも同様に書き換える。

function eraseBlock(y, x, r){
	
	var positions = otherYXs(y, x, r);
	
	eraseRect(y, x);
	eraseRect(positions[0], positions[1]);
	eraseRect(positions[2], positions[3]);
}

isStackedBlockはどうだろうか。これも同じように書き換えられる。

function isStackedBlock(y, x, r){

	var positions = otherYXs(y, x, r);
	
	var isStackedA = isStacked(y, x);
	var isStackedB = isStacked(positions[0], positions[1]);
	var isStackedC = isStacked(positions[2], positions[3]);
	
	return (isStackedA||isStackedB||isStackedC);
}

動作はサンプル13-1と同じだが、よりシンプルに書くことが出来た。

次はL字型のブロックだけ落ちてくるようにしよう。これができたら、棒状のケースと合体させて四角3つのブロックは完成だ。

L字型ブロックには回転数(r)によって、以下の4種類の配置がある。左からr = 0, 1, 2, 3に割り当てることにする。

       □  □
■□ □■ □■  ■□
□   □

さきほど作ったotherYXsを書き換えよう。

function otherYXs(y, x, r){
	var positions = [];
	
	switch(r){
		case 0:// 0度-г
			positions = [y, x+1, y+1, x];
			break;
		case 1://90度-¬
			positions = [y+1, x, y, x-1];
			break;
		case 2://180度-」
			positions = [y, x-1, y-1, x];
			break;
		case 3://270度-L 
			positions = [y-1, x, y, x+1];
			break;
	}
	return positions;
}

棒状とL字でotherYXsを2種類作れたので、これらを統合する。otherYXsを書き換えればよさそうだが、棒とL字のどちらの形にするか決める変数が必要になる。つまり今までは縦位置y, 横位置x, 回転数rの3つの変数で進めていたが、ここにブロックの種類sを追加する。s=0は棒、s=1はL字としよう。

othersYXsに引数sを追加し、switch文でs=0, 1の場合分けを行う。ifでなくswitchなのは、四角が4つになったときはブロックは2種類より多いためだ。今のうちから複数の場合分けに適したswitchで書いておこう。

function otherYXs(y, x, r, s){
	var positions = [];
	
	switch(s){
		case 0://棒
			switch(r){
				case 0:// 0度-横並び
					positions = [y, x+1, y, x-1];
					break;
				case 1://90度-縦並び
					positions = [y+1, x, y-1, x];
					break;
				case 2://180度-横並び
					positions = [y, x-1, y, x+1];
					break;
				case 3://270度-縦並び 
					positions = [y-1, x, y+1, x];
					break;
			}
			break;
		case 1://L字
			switch(r){
				case 0:// 0度-г
					positions = [y, x+1, y+1, x];
					break;
				case 1://90度-¬
					positions = [y+1, x, y, x-1];
					break;
				case 2://180度-」
					positions = [y, x-1, y-1, x];
					break;
				case 3://270度-L 
					positions = [y-1, x, y, x+1];
					break;
			}
			break;
	}
	return positions;
}

引数が4つになったので、drawBlock, eraseBlock, isStackedBlockも修正する。引数にsを追加するだけだ。

function drawBlock(y, x, r, s, color){
	
	var positions = otherYXs(y, x, r, s);
	
	drawRect(y, x, color);
	drawRect(positions[0], positions[1], color);
	drawRect(positions[2], positions[3], color);
}

最初に形を覚えておく変数Sを宣言しておく。dropRectとhandleKeyUpの中にあるdrawBlock, eraseBlock, isStackedBlockにも引数Sを追加する。

var X = 5;
var Y = 1;
var R = 0;// 回転数
var S = 0;// 形
drawBlock(Y, X, R, S, 'red');

Sの値の変更は積もったときに行う。dropRectの中に、S = 1 – Sを追加する。Sが0なら新しいSは1になり、Sが1なら、次のSは0になる。

X = 5; // 横5, 縦1の位置に、
Y = 1;
R = 0;
S = 1 - S; // 棒とLが交互

ところで、otherYXsはswitchが二重になっていて、しかもそれぞれcase 0~3まで4回ずつ同じようなことを書いているのが嫌な感じである。四角を4つにする前に、これをどうにかしよう。

以下の部分を整理する。

switch(s){
	case 0://棒
		switch(r){
			case 0:// 0度-横並び
				positions = [y, x+1, y, x-1]
				break;
			case 1://90度-縦並び
				positions = [y+1, x, y-1, x]
				break;
			case 2://180度-横並び
				positions = [y, x-1, y, x+1]
				break;
			case 3://270度-縦並び 
				positions = [y-1, x, y+1, x]
				break;
		}
		break;
	case 1://L字
		switch(r){
			case 0:// 0度-г
				positions = [y, x+1, y+1, x]
				break;
			case 1://90度-¬
				positions = [y+1, x, y, x-1]
				break;
			case 2://180度-」
				positions = [y, x-1, y-1, x]
				break;
			case 3://270度-L 
				positions = [y-1, x, y, x+1]
				break;
		}
		break;
}

sとrの値によって、[y1, x1, y2, x2]が決まることになる。表にまとめるとこうなる。yとxはそれぞれに出てくるので省略した。つまり、[y, x+1, y, x-1]は、[0, 1, 0, -1]と書ける。

s r y1 x1 y2 x2
0 0 0 1 0 -1
0 1 1 0 -1 0
0 2 0 -1 0 1
0 3 -1 0 1 0
1 0 0 1 1 0
1 1 1 0 0 -1
1 2 0 -1 -1 0
1 3 -1 0 0 1

この表を配列で表そう。sとrをインデックスにして[y1,x1,y2,x2]にアクセスできるようにする。

/* offsets[s][r]で、中心の四角(y, x)のときの
 他の2つの四角の[y1,x1,y2,x2]の(y, x)からの相対位置を求める */
 
var offsets =[[[ 0, 1, 0,-1],
        [ 1, 0,-1, 0],
        [ 0,-1, 0, 1],
        [-1, 0, 1, 0]],
       [[ 0, 1, 1, 0],
        [ 1, 0, 0,-1],
        [ 0,-1,-1, 0],
        [-1, 0, 0, 1]]];

これを利用するとotherYXsはこんな感じになる。

function otherYXs(y, x, r, s){
	var base = [y, x, y, x];
	var positions = base + offsets[s][r]; //こう書きたい
	
	return positions;
}

配列の要素同士の足し算を上のように書けると楽なのだが、JavaScriptではできないようなので、ループを使って書いた。

function otherYXs(y, x, r, s){
	var base = [y, x, y, x];
	var positions = [];
	
	for(var i = 0; i < base.length; i++){
		positions.push(base[i] + offsets[s][r][i]);
	}
	
	return positions;
}

配列offsetsはゲームの開始時に作っておく。これまでotherYXsが呼び出されるたびにswitch二重の条件判定を行っていたが、あらかじめ作っておいた結果を参照するだけでよくなった。otherYXsはdropRectやhandleKeyUpの中で使われているので、一定時間おきとキーボード操作のたびに呼び出されていたのだ。

次回、四角を4つにできれば完成となる。四角が4つになるとsの種類が2つより増える。つまりoffsetsのパターンが8つからもっと多くなることが予想される。

12. 四角を回す

ここまでで、「四角が1つずつ落ちてきて、その四角はキーボードで操作可能であり、一列積もると消える」ところまで作ることができた。

あとやることは、「四角が1つずつではなく、4つの四角からなるさまざまな形のブロックとして落ちてくる」ようにすることと、「そのブロックを回転できる」ようにすることだ。

まずは、四角が2つずつ落ちてくるようにしよう。

四角を落とす、キーボードで移動させるというのは、drawRect(y, x)とeraseRect(y, x)を使っていた。簡単のために2つの四角は常に横に並べるとしよう。drawRectとeraseRectを使っている箇所を、x と (x + 1) を使って2つセットで処理を行うように書き換えてみよう。

関係しそうな部分を抜き出すと

/* 最初の四角を描く部分 */
var X = 5;
var Y = 0;
drawRect(Y, X, 'red');
/* 一定時間ごとに四角を落とし、積もらせ、消す部分 */
function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		
		if(isStacked(Y+1,X)){// 下に積もっていれば、
			drawRect(Y, X); //この位置に積もらせた上で、
			
			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y);    // 列を消してから
			}
			X = 5;              // 横5, 縦0の位置に、 
			Y = 0;
		}else {// そうでなければ、1つ下に、
			Y += 1;
		}
		
		drawRect(Y, X, 'red'); // 四角を描く
		
	}, t);
}
/* キーボードで四角を動かす部分 */
function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);
	
	switch(key) {
		case 39://右
			if(!isStacked(Y,X+1)) X += 1;
			break;
		case 37://左
			if(!isStacked(Y,X-1)) X -= 1;
			break;
		case 38://上
			if(Y > 0) Y -= 1;
			break;
		case 40:// 下
			if(!isStacked(Y+1,X)) Y += 1;
			break;
	}
		
	drawRect(Y, X, 'red');
	
}

これを書き換えていく。

/* 最初の四角を描く部分 */
var X = 5;
var Y = 0;
drawRect(Y, X, 'red');
drawRect(Y, X+1, 'red');

/* 一定時間ごとに四角を落とし、積もらせ、消す部分 */
function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		eraseRect(Y, X+1);
		
		if(isStacked(Y+1,X)){// 下に積もっていれば、
			drawRect(Y, X); //この位置に積もらせた上で、
			drawRect(Y, X+1); //この位置に積もらせた上で、

			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y);    // 列を消してから
			}
			X = 5;              // 横5, 縦0の位置に、
			Y = 0;
		}else {// そうでなければ、1つ下に、
			Y += 1;
		}
		
		drawRect(Y, X, 'red'); // 四角を描く
		drawRect(Y, X+1, 'red'); // 四角を描く
	}, t);
}
/* キーボードで四角を動かす部分 */
function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);
	eraseRect(Y, X+1);

	switch(key) {
		case 39://右
			if(!isStacked(Y,X+1)) X += 1;
			break;
		case 37://左
			if(!isStacked(Y,X-1)) X -= 1;
			break;
		case 38://上
			if(Y > 0) Y -= 1;
			break;
		case 40:// 下
			if(!isStacked(Y+1,X)) Y += 1;
			break;
	}
		
	drawRect(Y, X, 'red');
	drawRect(Y, X+1, 'red');
	
}

さらに、isStackedの部分は、xの位置だけでなく、x+1の位置にも適用する必要があるので、

/* 一定時間ごとに四角を落とし、積もらせ、消す部分 */
function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		eraseRect(Y, X+1);
		
		if(isStacked(Y+1,X)||isStacked(Y+1,X+1)){// 下に積もっていれば、
			drawRect(Y, X); //この位置に積もらせた上で、
			drawRect(Y, X+1); //この位置に積もらせた上で、

			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y); // 列を消してから
			}
			X = 5;           // 横5, 縦0の位置に、 
			Y = 0;
		}else {// そうでなければ、1つ下に、
			Y += 1;
		}
		
		drawRect(Y, X, 'red'); // 四角を描く
		drawRect(Y, X+1, 'red'); // 四角を描く
	}, t);
}
/* キーボードで四角を動かす部分 */
function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);
	eraseRect(Y, X+1);

	switch(key) {
		case 39://右
			if(!isStacked(Y,X+2)) X += 1;
			break;
		case 37://左
			if(!isStacked(Y,X-1)) X -= 1;
			break;
		case 38://上
			if(Y > 0) Y -= 1;
			break;
		case 40:// 下
			if(!isStacked(Y+1,X)&&!isStacked(Y+1,X+1)) Y += 1;
			break;
	}
		
	drawRect(Y, X, 'red');
	drawRect(Y, X+1, 'red');
	
}

これで四角が横並びで2つずつ落ちてきて、キーボードで操作でき、一列積もったら消えるようになった。

次はこの2つの四角を回転できるようにする。キーボードの上を押したときに、左側の四角を中心に時計回りに回転するようにしよう。

2つの四角をA,Bとし、Aを■、Bを□で表すと、■□はキーボードの上を押すたびに下図のように位置を変える。

           □
■□ → ■ → □■ → ■
    □

ここで、A,Bの位置(y,x)をA(0,0), B(0,1)とすると、キーボードの上を一回押したとき、それぞれの四角の位置は、A(0,0), B(1,0)になり、もう一度上を押すと、A(0,0), B(0,-1)となり、さらにもう一度押すと、A(0,0), B(-1,0)となり、4回目でA(0,0), B(0,1)に戻ってくる。

つまり、回転するとき、左側(A)の四角はそのままで、右側(B)の四角だけ、位置を変えればよい。A(y,x)の y = Y, x = X のとき、回転させた回数とBの位置を表にまとめた。なお0回と4回は四角の位置は同じである。

0回 1回 2回 3回 4回
A(0,0)のとき、Bの(y, x) (0, 1) (1, 0) (0, -1) (-1, 0) (0, 1)
A(Y, X)のとき、Bの(y, x) (Y, X+1) (Y+1, X) (Y, X-1) (Y-1, X) (Y, X+1)

ではコードを書こう。回転に関係ない部分は削除して、最低限必要な部分のみ抜粋して試してみよう。drawRectとeraseRectは必要だが、四角を落としたり積もらせたり列を消したりする部分はここでは必要ない。キーボードの操作も上キー以外の部分は不要である。

/* 四角を2つ描く部分 */
var X = 5;
var Y = 0;
drawRect(Y, X, 'red');
drawRect(Y, X+1, 'red');
/* キーボードの上で回転させる部分 */
function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	switch(key) {
		case 38://上
			//【回転させる】
			break;
	}
	
}

キーボードの上を押した回数を変数Rに覚えておき、Rの値によって、四角Bの位置を書き換えるようにする。

var X = 5;
var Y = 5; 
var R = 0; // 回転の回数を変数Rに覚えておく
drawRect(Y, X, 'red');
drawRect(Y, X+1, 'red');

キーボードの上を押したときにRをカウントアップする。R = 4とR = 0のとき、四角Bの位置は同じなので、Rが4以上にならないように3を超えたら0に戻しておく。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	switch(key) {
		case 38://上
		
			switch(R){
				case 0:
					//Aの下に動かす
					break;
				case 1:
					//Aの左に動かす
					break;
				case 2:
					//Aの上に動かす
					break;
				case 3:
					//Aの右に動かす
					break;
			}
		
			R = R + 1;
			if (R > 3) R = 0;
			break;
	}
	
}

R = 0 のときの「Aの下に動かす」というのは、Bが(Y, X+1)の位置にあるとき、(Y, X+1)の四角を消して、(Y+1, X)に新しく四角を描けばいいので、

//Aの下に動かす
eraseRect(Y, X+1);
drawRect(Y+1, X);

同様に

switch(R){
	case 0:
		//Aの下に動かす
		eraseRect(Y, X+1);
		drawRect(Y+1, X);
		break;
	case 1:
		//Aの左に動かす
		eraseRect(Y+1, X);
		drawRect(Y, X-1);
		break;
	case 2:
		//Aの上に動かす
		eraseRect(Y, X-1);
		drawRect(Y-1, X);
		break;
	case 3:
		//Aの右に動かす
		eraseRect(Y-1, X);
		drawRect(Y, X+1);
		break;
}

四角Bは白色になっている。drawRect(Y+1, X)は本来drawRect(Y+1, X, ‘red’)と描く必要があった。どちらの四角が動いているかわかりやすいように、ここではこのままにしておこう。

さて、上キーで四角を回転できるようになったので、これをこれまでのコードに統合してみよう。うまくいくだろうか。

単純に四角を回転させるコードを追加するだけではうまくいかない。四角を落とす部分や積もらせる部分は2つの四角が横に並んでいることを前提に書いていたからだ。

対応策としてすぐに思いつくのは、回転と同じように、四角を落とす部分や積もらせる部分も、Rの値によって場合分けして書く方法か。

drawRect(Y, X+1)やeraseRect(Y, X+1)としているところはR = 0を前提としている部分なので、こういう部分全部をswitch文でR=1~3の場合もそれぞれ書けばよい。

落ちてくる四角を回転させることが出来るようになったが、積もる部分で何かがおかしい。isStackedの部分では回転した後の状態を考慮していないからだ。それに、似たようなswitch文を何箇所にも書くのは面倒なのでどうにかしたい。

ここで、回転を追加する前の12-1の状態に戻って、コードを整理する。

問題はdropRect関数やhandleKeyUp関数の中にある条件部分だろう。

/* dropRect関数の一部 */
if(isStacked(Y+1,X)||isStacked(Y+1,X+1)){// 下に積もっていれば、
/* handleKeyUp関数の一部 */
switch(key) {
	case 39://右
		if(!isStacked(Y,X+2)) X += 1;
		break;
	case 37://左
		if(!isStacked(Y,X-1)) X -= 1;
		break;
	case 38://上
		if(Y > 0) Y -= 1;
		break;
	case 40:// 下
		if(!isStacked(Y+1,X)&&!isStacked(Y+1,X+1)) Y += 1;
		break;
}

横に並んだ2つの四角に対して、isStacked関数を使って「移動先に四角がなければ」という条件で以降の処理を行っている。横並びの2つの四角だけならこれで問題ないが、最終的には四角は4つになるし、並び方も何通りもあるので、そのそれぞれのパターンに対して個別に条件を用意するのは大変だ。

「移動先に四角がなければ動かす」という考え方をやめて、動かした後に、「そこが既に四角がある場所だったら元の位置に戻す」としてみよう。この考えの下、handleKeyUp関数を書き換える。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	var deltaX = 0;
	var deltaY = 0;
	
	eraseRect(Y, X);
	eraseRect(Y, X+1);
	
	switch(key) {
		case 39://右
			deltaX = 1;
			break;
		case 37://左
			deltaX = -1;
			break;
		case 40:// 下
			deltaY = 1;
			break;
		case 38://上
			// 回転
			break;
	}
	
	X += deltaX;
	Y += deltaY;
	
	if(isStacked(Y, X)||isStacked(Y, X+1)){
		X -= deltaX;
		Y -= deltaY;
	}
	
	drawRect(Y, X, 'red');
	drawRect(Y, X+1, 'red');
	
}

移動量を表す変数deltaXとdeltaYを追加することで、switch文がシンプルになり(上キーの場合は回転用なのでここでは省略しておく)、if(isStacked(○○))の部分も一箇所にまとめることができた。

dropRect関数も同じように書き換えよう。

/* 書き換え前 */
function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		eraseRect(Y, X+1);
		
		if(isStacked(Y+1,X)||isStacked(Y+1,X+1)){// 下に積もっていれば、
			drawRect(Y, X); //この位置に積もらせた上で、
			drawRect(Y, X+1); //この位置に積もらせた上で、
			
			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y);    // 列を消してから
			}
			X = 5;              // 横5, 縦0の位置に、
			Y = 0;
		}else {// そうでなければ、1つ下に、
			Y += 1;
		}
		
		drawRect(Y, X, 'red'); // 四角を描く
		drawRect(Y, X+1, 'red'); // 四角を描く
		
	}, t);
}
/* 書き換え後 */
function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		eraseRect(Y, X+1);
		
		Y += 1;// まず落下させる
		
		if(isStacked(Y,X)||isStacked(Y,X+1)){// 落下先が積もっていれば、
			Y -= 1; //落下は取りやめ
			
			drawRect(Y, X); //この位置に積もらせた上で、
			drawRect(Y, X+1); //この位置に積もらせた上で、
			
			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y);    // 列を消してから
			}
			X = 5;              // 横5, 縦0の位置に、
			Y = 0;
		}
		
		drawRect(Y, X, 'red'); // 四角を描く
		drawRect(Y, X+1, 'red'); // 四角を描く
		
	}, t);
}

動き自体はサンプル12-1と変わっていないが、コードは幾分かシンプルになった。重要なのは、四角を描く、四角を消す、既に四角があるか調べる関数の引数が以下のように共通化できたことだろう。

/* 四角を描く */
drawRect(Y, X);
drawRect(Y, X+1);

drawRect(Y, X, 'red');
drawRect(Y, X+1, 'red');


/* 四角を消す */
eraseRect(Y, X);
eraseRect(Y, X+1);


/* 既に四角があるか調べる */
if(isStacked(Y,X)||isStacked(Y,X+1))

2つの四角の位置は(Y, X)と(Y, X+1)で特定している。
四角Bが(Y, X+1)となるのは2つの四角が横並びのときで、縦並びのときは(Y+1, X)または(Y-1, X)となる。横並びか縦並びかは回転数Rの値による。

2つの四角を描く関数をまとめて、1つのブロックとして描く関数drawBlockとしよう。ブロック(2つの四角)の形は、四角Aと、四角Aに対する四角Bの相対位置で決められる。四角Bの相対位置は回転の値で決まるので、関数の引数は四角Aの縦位置y, 横位置x, 回転数rにしよう。

function drawBlock(y, x, r){
	switch(r){
		case 0:// 0度-横並び
			drawRect(y, x);
			drawRect(y, x+1);
			break;
		case 1://90度-縦並び
			drawRect(y, x);
			drawRect(y+1, x);
			break;
		case 2://180度-横並び
			drawRect(y, x);
			drawRect(y, x-1);
			break;
		case 3://270度-縦並び 
			drawRect(y, x);
			drawRect(y-1, x);
			break;
	}
}

switch文のすべてにdrawRect(y, x)が含まれるので、switchの外に出して、

function drawBlock(y, x, r){
	drawRect(y, x);
	switch(r){
		case 0:// 0度-横並び
			drawRect(y, x+1);
			break;
		case 1://90度-縦並び
			drawRect(y+1, x);
			break;
		case 2://180度-横並び
			drawRect(y, x-1);
			break;
		case 3://270度-縦並び 
			drawRect(y-1, x);
			break;
	}
}

同じように、eraseBlock関数を定義する。drawをeraseに書き換えるだけだ。

function eraseBlock(y, x, r){
	eraseRect(y, x);
	switch(r){
		case 0:// 0度-横並び
			eraseRect(y, x+1);
			break;
		case 1://90度-縦並び
			eraseRect(y+1, x);
			break;
		case 2://180度-横並び
			eraseRect(y, x-1);
			break;
		case 3://270度-縦並び 
			eraseRect(y-1, x);
			break;
	}
}

続いてisStackedBlockも作ろう。これは少し考える必要がありそうだ。引数は四角Aの縦位置y, 横位置x, 回転数rとするが、この関数で何をしたいかというと、与えられた引数で決まる位置(四角2つ分の座標のいずれか)に、既に別の四角がある場合にtrueを、両方とも空いている場合にfalseを返すようにすればよい。

function isStackedBlock(y, x, r){
	var isStackedA = isStacked(y, x);
	var isStackedB = false;
	
	switch(r){
		case 0:// 0度-横並び
			isStackedB = isStacked(y, x+1);
			break;
		case 1://90度-縦並び
			isStackedB = isStacked(y+1, x);
			break;
		case 2://180度-横並び
			isStackedB = isStacked(y, x-1);
			break;
		case 3://270度-縦並び 
			isStackedB = isStacked(y-1, x);
			break;
	}
	
	return (isStackedA||isStackedB);
}

それでは、drawBlock, eraseBlock, isStackedBlockを使って、サンプル12-5のコードを書き換えよう。

ブロックの初期値を定義している部分に回転数の変数Rを追加する。また、Y = 0だったところをY = 1に変更した。これは回転したときに縦位置がマイナス値になってしまわないようにするための対処だ。drawRectを2つ書いていた部分はそのままでもよいが、drawBlockに変更しておこう。

var X = 5;
var Y = 1;
var R = 0;
drawBlock(Y, X, R);

次はdropRect関数だ。eraseRectはeraseBlockに、drawRectはdrawBlockに、isStackedはisStackedBlockにそれぞれ書き換える。2つの四角のためにそれぞれ2つずつ関数を書いていた部分がひとつの関数に置き換わるので、わかりやすくなる。

function dropRect(t){
	var drop = setInterval(function(){
		eraseBlock(Y, X, R);
		
		Y += 1;// まず落下させる
		
		if(isStackedBlock(Y, X, R)){// 落下先が積もっていれば、
			Y -= 1; //落下は取りやめ
			
			drawBlock(Y, X, R); //この位置に積もらせた上で、
			
			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y);    // 列を消してから
			}
			X = 5;              // 横5, 縦1の位置に、
			Y = 1;
			R = 0;
		}
		
		drawBlock(Y, X, R); // ブロックを描く
		
	}, t);
}

handleKeyUp関数については、回転数Rを追加したのに合わせて、回転量deltaRも追加しよう。12-2で書いたような回転させるための長いコードはdeltaR = 1とするだけで済む。Rの値にdeltaRを足したり引いたりする部分では、Rが3を超えたりマイナスにならないように対処しておく。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	var deltaX = 0;
	var deltaY = 0;
	var deltaR = 0;
	
	eraseBlock(Y, X, R);
	
	switch(key) {
		case 39://右
			deltaX = 1;
			break;
		case 37://左
			deltaX = -1;
			break;
		case 40:// 下
			deltaY = 1;
			break;
		case 38://上(回転)
			deltaR = 1;
			break;
	}
	
	X += deltaX;
	Y += deltaY;
	R += deltaR;
	if(R > 3) R = 0;
	
	if(isStackedBlock(Y, X, R)){
		X -= deltaX;
		Y -= deltaY;
		R -= deltaR;
		if(R < 0) R = 3;
	}
	
	drawBlock(Y, X, R);
	
}

落ちてくるブロック(2つの四角)は白色だが、キーボードで操作できるし、回転できるし、ちゃんと積もるようになった。落ちてくるブロックを赤色で表示するにはdrawBlock関数に色を指定する引数を追加すればよいので、次はこれをやろう。

drawBlock関数の引数にcolorを追加する。handleKeyUp関数とdropRect関数の中で使用しているdrawBlcokにも’red’を追加すればよい。JavaScriptでは関数の引数が不足していてもエラーにならないので、ブロックの色を白色のままにしたい場合はcolor引数を省略すればよい。

function drawBlock(y, x, r, color){
	drawRect(y, x, color);
	switch(r){
		case 0:// 0度-横並び
			drawRect(y, x+1, color);
			break;
		case 1://90度-縦並び
			drawRect(y+1, x, color);
			break;
		case 2://180度-横並び
			drawRect(y, x-1, color);
			break;
		case 3://270度-縦並び 
			drawRect(y-1, x, color);
			break;
	}
}

/* dropRect関数の一部 */
drawBlock(Y, X, R, 'red'); // 四角を描く

あとうまくいっていないのは列を消す部分だ。実はこのままではブロックを2列分積もらせても1列分しか消えない。

列を消す処理はdropRect関数の中にある以下の部分だった。

/* dropRectの一部 */
if(isStackedRow(Y)){// その列が1列積もっていれば
	eraseRow(Y); // 列を消してから
}

eraseRowはeraseRectとslideRectをまとめたもので

function eraseRow(y){
	for(var x = 1; x < 11; x++){
		eraseRect(y, x);
		slideRect(y, x);
	}
}

function slideRect(y, x){
	for(var count = y; count > 0; count--){
		if(isStacked(count - 1, x)){
			eraseRect(count - 1, x);
			drawRect(count, x);
		}
	}
}

つまり、四角が積もったときに四角の縦位置yを与えて、その位置の列がすべて埋まっていればその列を消すという処理だった。今ブロックは2つの四角からできているので、与える縦位置も2つ必要になる。eraseRectからeraseBlockを作ったように、eraseRowからeraseBlockRowという関数を作り、引数には四角Aの縦位置Yと回転数Rを与えればよさそうだ。

と思ったが、実は四角が積もったときにその四角の縦位置を与える必要はない。四角が積もったとき、全部の列を調べて、1列積もっていればその列を消せばよいと考えれば、引数は不要になる。これをeraseRowsとして、大まかには以下のようになる。

function eraseRows(){
	for(すべての縦位置について繰り返す){
		if(その縦位置が一列積もっていれば){
			その列を消す
			その列より上の四角を全部ずらす
		}
	}
}

dropRectの中にあったif(isStackedRow(Y))という条件はeraseRowsの「その縦位置が一列積もっていれば」と同じことなので、dropRectのほうではeraseRows()とだけ書けばよい。「その列を消す」と「その列より上の四角を全部ずらす」はeraseRow(y)そのものなので

function eraseRows(){
	for(var y = 0; y < 19; y++){
		if(isStackedRow(y)) eraseRow(y);
	}
}

今回はずいぶん長くなってしまったが、これで、

  • 2つの四角がひとつのブロックとして落ちてくる
  • ブロックの左右方向と下方向の移動はキーボードで操作できる
  • キーボードの上キーでブロックを時計回りに回転させる
  • 底や既にブロックが積もった位置にブロックが到達するとそこにブロックが積もる
  • 一列積もると列が消え、その上のブロックが下にずれる

ここまでできた。あとは、ブロックを2つの四角ではなく、4つの四角で作ることが出来れば完成だ。

11. 列を消す

前回までで四角が落ちてきて積もるようになった。今回は横に1列積もったらその列を消すようにしよう。

縦y, 横xの位置にある四角を消すコードはeraseRect(y, x)だった。列を消す条件は、「積もった四角と同じ縦位置に1列分四角が積もっているなら」となる。縦y, 横xの位置に既に四角があるかどうかはisStacked(y, x)で調べられる。

具体的にy = 18のときで考えると、xは1~10までなので、こんな感じのコードになる。

if(isStacked(18, 1)かつisStacked(18, 2)かつ・・・isStacked(18, 10)){
	eraseRect(18, 1);
	eraseRect(18, 2);
	 ...
	eraseRect(18, 10);
}

「1列積もっていたら」をisStacked(y, x)を使ってまとめよう。x = 1からx = 10まですべてのisStackedがtrueのときに全体としてtrueとなるように、論理積を使う。

function isStackedRow(y){// 縦yの列が1列積もっていればtrueを返す
	var allStacked = true;
	
	for(var x = 1; x < 11; x++){
		allStacked = allStacked && isStacked(y, x);
	}
	
	return allStacked
}

1列消す関数も用意する。

function eraseRow(y){
	for(var x = 1; x < 11; x++){
		eraseRect(y, x);
	}
}

これらを使って、dropRect関数を書き換える。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		
		if(isStacked(Y+1,X)){// 下に積もっていれば、
			drawRect(Y, X);  // この位置に積もらせた上で、
			
			if(isStackedRow(Y)){// その列が1列積もっていれば
				eraseRow(Y);    // 列を消してから
			}
			X = 5;             // 横5, 縦0の位置に、
			Y = 0;
		}else {                 // そうでなければ、1つ下に、
			Y += 1;
		}
		
		drawRect(Y, X, 'red');  // 四角を描く
		
	}, t);
}

次は、列を消したとき、消えた列の上に乗っていた四角を落とす。以下のような処理を、消える列の四角それぞれに対して行えばよい。

消えた四角のひとつ上に四角があるなら
 ひとつ上の四角を消す
 消えた列の位置に四角を描く

この処理は「落とす」と区別できるように「ずらす」と表現し、slideRectという関数にする。

function slideRect(y, x){
	if(isStacked(y-1, x)){
		eraseRect(y-1, x);
		drawRect(y, x);
	}
}

slideRectは列が消えるときに動いてほしいので、eraseRow関数に組み込む。

function eraseRow(y){
	for(var x = 1; x < 11; x++){
		eraseRect(y, x);
		slideRect(y, x);
	}
}

しかし、これでは消える列の上に四角が2つ以上積み上がっている場合には、2つ目以上の四角はその位置に残ってしまう(消える列のすぐひとつ上の四角しかずれてくれない)。消える列の上の四角すべてにずらす処理を行う必要がある。

縦yの列が消えるとき、ひとつ上(y-1)の四角を下にずらす。2つ上の四角も下にずらす。3つ上の四角も下にずらす。これを一番上(y=0)の四角まで繰り返す。

if(isStacked(y-1, x)){
	eraseRect(y-1, x);
	drawRect(y, x);
}

if(isStacked(y-2, x)){
	eraseRect(y-2, x);
	drawRect(y-1, x);
}

if(isStacked(y-3, x)){
	eraseRect(y-3, x);
	drawRect(y-2, x);
}

(略)

if(isStacked(0, x)){
	eraseRect(0, x);
	drawRect(1, x);
}

isStackedとeraseRectのほうの縦位置はy-1から始まって0まで、drawRectのほうはyから始まって1まで順に実行するので、forループのカウンタにyを使ってまとめると

for(var count = y; count > 0; count--){
	if(isStacked(count - 1, x)){
		eraseRect(count - 1, x);
		drawRect(count, x);
	}
}

これを使ってslideRect関数を書き換える。

function slideRect(y, x){
	for(var count = y; count > 0; count--){
		if(isStacked(count - 1, x)){
			eraseRect(count - 1, x);
			drawRect(count, x);
		}
	}
}

動作確認のために何列か四角を積む必要があるが、面倒なのであらかじめ積んでおこう。

/* テスト用 あらかじめ四角を積んでおく */
for(var x = 1; x < 11; x++){
	for(var y = 15; y < 19; y++){
		if(x != 5) drawRect(y, x);
	}
}

この方法では四角をひとつだけずらすslideRect関数を四角を全部ずらすように書き換えていた。別の方法としては、四角を全部ずらすまでslideRect関数を繰り返し使うという方法が考えられる。

/* 書き換える前 */
function slideRect(y, x){
	if(isStacked(y-1, x)){
		eraseRect(y-1, x);
		drawRect(y, x);
	}
}

slideRect関数の名前をslideRectOnceに変更し、slideRectOnceを繰り返し使う関数を改めてslideRect関数として定義する。

/* 名前を変えた */
function slideRectOnce(y, x){
	if(isStacked(y-1, x)){
		eraseRect(y-1, x);
		drawRect(y, x);
	}
}

縦位置yの位置から始まって、最上部までslideRectOnceを繰り返す。y = 0のとき、その上(y-1)は領域外になるので繰り返し条件はy > 0とする。

function slideRect(y, x){
	while(y > 0){
		slideRectOnce(y, x);
		y--;
	}
}

10. 四角を積み上げる

今回は落ちてきた四角が壁や底を突き抜けないようにする。まず思いつくのは、四角の縦位置、横位置を変更しているところに実行条件をつけることだろう。

四角を描く領域は縦20マス、横12マスである。縦については、y = 0~19まで(底がy = 19)なので、yの値を増やす処理に対して、y < 18のとき、という条件をつければ、底を突き抜けて四角が落ちることはなくなる。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		if(Y < 18) Y += 1;
		drawRect(Y, X, 'red');
	}, t);
}

横の位置については、x = 0のときとx = 11のときは壁である。四角を動かす関数に対して、同様に条件をつければ壁を突き抜けることはない。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);
	
	switch(key) {
		case 39://右
			if(X < 10) X += 1;
			break;
		case 37://左
			if(X > 1) X -= 1;
			break;
		case 38://上
			if(Y > 0) Y -= 1;
			break;
		case 40:// 下
			if(Y < 18) Y += 1;
			break;
	}
		
	drawRect(Y, X, 'red');
}

これで、落ちてきた四角を底で止めることができた。では、次に新しい四角が落ちてきて、既に底で止まっている四角の一つ上に来たらどうなるか。

既にある四角の上に新しい四角が積まれてほしいところだが、四角を落とす条件がif(Y < 18)であるため、四角は底に到達する(Y ≧ 18になる)まで止まらない。

つまり、本来、四角を止めるために必要な条件は「四角が底に来たら」ではなく、「既に四角がある場所に到達したら」である。

さて、あるマスに四角が既に存在しているかをどうやって判定するか。縦y 横xの位置に四角を描くとき、以下のコードを使っていた。

blocks[y][x].setAttribute('class', 'on');

これは縦20マス、横12マスのテーブルの縦y, 横xの位置にあるtd要素にclass=”on”をセットすることである。つまり、class=”on”のtd要素には四角が描かれているということだ。これを条件に使えばよい。

まずは縦方向だけ考えよう。落ちてきた赤い四角は底に到達すると白色になる。次に新しい赤い四角が落ちてきて、先ほど白くなった四角の上に積もって、これもまた白くなる。

落ちてくる四角は赤色、積もった四角は白色で表すようにdropRect関数を書き換える。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		if(【一つ下に白い四角がないなら】){
			Y += 1;
			drawRect(Y, X, 'red');
		}else {
			drawRect(Y, X); // 四角が積もる
		}
	}, t);
}

落ちてきた四角が積もった(白くなった)ら、次の赤い四角を落ちてこさせる。縦位置を0に戻して、再び赤い四角を描けばいい。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		if(【一つ下に白い四角がないなら】){
			Y += 1;
			drawRect(Y, X, 'red');
		}else {
			drawRect(Y, X); // 四角が積もる
			// 新しい四角を落下させる
			X = 5;
			Y = 0;
			drawRect(Y, X, 'red');
		}
	}, t);
}

落下させる条件は【一つ下に白い四角がないなら】としたいが、ここまでの動きを確認するために、if(Y < 18)に戻して動かしてみる。

「一つ下に白い四角があるかどうか」を判定するために、縦y, 横xの位置に四角があればTrueを、そうでなければFalseを返す関数を用意する。

function isStacked(y,x){// 縦y, 横xの位置に四角があればtrueを返す
	if(blocks[y][x].className == 'on'){
		return true;
	}else {
		return false;
	}
}

この関数をdropRect関数に組み込む。isStackedがFalseのときに実行したいので、!をつけて論理値を反転させる。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		if(!isStacked(Y+1,X)){// 下に積もっていなければ落とす
			Y += 1;
			drawRect(Y, X, 'red');
		}else {
			drawRect(Y, X);// 四角が積もる
			// 新しい四角を落下させる
			X = 5;
			Y = 0;
			drawRect(Y, X, 'red');
		}
	}, t);
}

上記コードはifとelseの両方の分岐でdrawRect(Y, X, ‘red’)が使われているので、これを外に出そう。また!isStackedというのはわかりにくいので、ifとelseの内容を逆にする。整理すると

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		
		if(isStacked(Y+1,X)){// 下に積もっていれば、
			drawRect(Y, X); //この位置に積もらせた上で、
			// 横5, 縦0の位置に、
			X = 5; 
			Y = 0;
		}else {// そうでなければ、1つ下に、
			Y += 1;
		}
		
		drawRect(Y, X, 'red'); // 四角を描く

	}, t);
}

次はキーボードでの移動にも同じ判定条件を追加する。いまのままでは、積もった四角に対して「横から当てる」ように赤い四角を動かすと、四角が重なってしまう。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);
	
	switch(key) {
		case 39://右
			if(!isStacked(Y,X+1)) X += 1;
			break;
		case 37://左
			if(!isStacked(Y,X-1)) X -= 1;
			break;
		case 38://上
			if(Y > 0) Y -= 1;
			break;
		case 40:// 下
			if(!isStacked(Y+1,X)) Y += 1;
			break;
	}
		
	drawRect(Y, X, 'red');
	
}

9. ここまでのまとめ

第8章までで、落ちてくる四角をキーボードで動かせるところまで作った。本書第9章ではSunabaに「実行条件」が導入される。if文はJavaScriptではここまでにすでに使っているので、今回はとくに新しいことはない。

そこで今回は、ここまでのまとめを行う。4番目はおまけだ。

  1. 壁と底を描く。
  2. キーボードで四角を動かせるようにする。
  3. 自動的に四角が落ちてくるようにする。
  4. 落ちてくる四角は赤色で表示する。

まず四角描くための座標として、tableを生成する。

var body = document.getElementsByTagName('body')[0];
var table = document.createElement('table');

body.appendChild(table);

/* 縦20マス、横12マスのテーブルを作る
  各td要素はその位置に対応したidを持つ
  各td要素は二次元配列blocksでアクセスできる */
var blocks = [];
for(var y = 0; y < 20; y++){
	var trs = [];
	var tr = document.createElement('tr');
	table.appendChild(tr);
	
	for(var x = 0; x < 12; x++){
		var td = document.createElement('td');
		
		var value = 'id';
		value += ('0' + y).slice(-2);
		value += ('0' + x).slice(-2);
		td.setAttribute('id', value);
		
		tr.appendChild(td);
		trs.push(td)
	}
	blocks.push(trs);
}

table内のtd要素を縦y,横xで指定し、class属性に値”on”を与えることで、CSSにてtd要素の色を変え、四角を描く。四角を描く関数drawRect、消す関数eraseRect、四角を落とす関数dropRectを用意する。

function drawRect(y, x){
	blocks[y][x].setAttribute('class', 'on');
}

function eraseRect(y, x){
	blocks[y][x].removeAttribute('class');
}

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		Y += 1;
		drawRect(Y, X);
	}, t);
}

イベントリスナーの関数を用意し、

function addEvent(obj, type, fn){
	if(obj && obj.addEventListener){// W3C
		obj.addEventListener(type, fn, false);
	}else if(obj && obj.attachEvent){// 古いIE
		obj.attachEvent('on' + type, fn);
	}
}

キーボードが押されたらhandleKeyUp関数が実行されるようにする。

addEvent(document, 'keyup', handleKeyUp);

壁と底は、関数drawRectで四角を描くことで作る。

/* 左右の壁を描く */
for(var y = 0; y < 20; y++){
	drawRect(y, 0); // 左の壁
	drawRect(y, 11);// 右の壁
}

/* 底を描く */
for(var x = 1; x < 11; x++){
	drawRect(19, x);
}

さて、動かせる四角は赤色にし、壁と底は白色としたい。四角の色はCSSで指定しており、具体的にはclass=”on”のとき、白色としていた。

td.on {
  background: #fff;
}

以下のようなCSSを用意しておけば、setAttributeメソッドで与えるclass属性値によって、四角の色を変えられる。

td.white {
 background: #fff;
}

td.red {
  background: #f00;
}

関数drawRectの引数は、縦位置yと横位置xだったが、これに色を示す引数”color”を加えればできそうだ。

function drawRect(y, x){
	blocks[y][x].setAttribute('class', 'on');
}

しかし、関数のフォーマットを変えてしまうと、すでに使われている関数も書き直す必要があり面倒なので、引数colorにデフォルト値を設定しておき、引数を省略した場合はこれまでどおり、”on”が設定されるようにしたい。

function drawRect(y, x, color='on'){/* JavaScriptではこういう書き方はできない */
	blocks[y][x].setAttribute('class', color);
}

実は、JavaScriptの関数では引数にデフォルト値を持たせることはできない。一方、引数がなくてもエラーとならないので、以下のように書けば擬似的にデフォルト値を持たせることができる。

function drawRect(y, x, color){
	if(typeof color == 'undefined'){
		blocks[y][x].setAttribute('class', 'on');
	} else {
		blocks[y][x].setAttribute('class', color);
	}
}

handleKeyUp関数も調整する。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);

	if(key == 39){// 右 
		X = X + 1;
	}
	if(key == 37){// 左 
		X = X - 1;
	}
	if(key == 38){// 上 
		Y = Y - 1;
	}
	if(key == 40){// 下 
		Y = Y + 1;
	}
	
	drawRect(Y, X);
	
}

drawRectの引数に色を追加するのと、if文で書いていた条件をswitch文に書き換える。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);
	
	switch(key) {
		case 39://右
			X += 1;
			break;
		case 37://左
			X -= 1;
			break;
		case 38://上
			Y -= 1;
			break;
		case 40:// 下
			Y += 1;
			break;
	}
		
	drawRect(Y, X, 'red');
	
}

dropRect関数のほうもdrawRectに赤色を設定しよう。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		Y += 1;
		drawRect(Y, X, 'red');
	}, t);
}

最後に、最上部の中央あたりに赤い四角を描き、四角が落ちるようにdropRect関数を呼び出す。

var X = 5;
var Y = 0;
drawRect(Y, X, 'red');
dropRect(500);

8. 四角を操作する

次はキーボードの入力で四角を動かせるようにする。Sunabaでは50004番~50007番のメモリがキーボードの上下左右キーにつながっており、たとえば50004番のメモリが1のときは上キーが押されている状態、0のときは押されていない状態を意味していた。

JavaScriptで、キーが押されたときに何かするには「イベント処理」のやり方を知る必要がある。

W3C DOMでは、あるオブジェクトにおける”イベント”の発生をトリガーとして特定の関数を実行するためには、addEventListner()メソッドを使い、オブジェクトとイベントおよび関数を関連づける。

addEventListener(イベントタイプ, 関数名, イベント段階)

イベントタイプはたとえば「キーが押された」などのイベントの種類、関数名はイベントが起こったときに実行する関数の名前である。イベント段階はさしあたりfalseを指定しておく。古いIEと最近のブラウザでは利用可能なメソッドに若干違いがあるため、クロスブラウザに対応したイベント割り当て用の関数を定義する。

function addEvent(obj, type, fn){
	if(obj && obj.addEventListener){// W3C
		obj.addEventListener(type, fn, false);
	}else if(obj && obj.attachEvent){// 古いIE
		obj.attachEvent('on' + type, fn);
	}
}

windowがロードされたときにinit関数を実行するようにイベントリスナーを作成するコードは

window.addEventListener('load', init, false);

クロスブラウザ対応版では

addEvent(window, 'load', init);

キーボードが押されたとき(より正確に言うと、押されたキーが離されたとき)、ある関数が実行されるようにする。以下のコードは押されたキーが離されたときのイベントであるkeyupに、handleKeyUp関数を関連付ける。

addEvent(document, 'keyup', handleKeyUp);

function handleKeyUp(e){
	var key = e.keyCode;
	console.log(key);
}

handleKeyUp()関数は発生したイベントを表す引数を受け取る。この引数は通常eで表す。これもクロスブラウザを考慮した書き方をすると

if(typeof e == 'undefined') e = window.event; // IEはeではなくwindow.event 

押されたキーのキーコードはkeyupイベントのkeyCodeプロパティに保持される。keyCodeをコンソールに出力して確認する。

addEvent(document, 'keyup', handleKeyUp);

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	console.log(key);
}

押されたキーのkeyCodeをコンソールから確認すると、

←:37
↑:38
→:39
↓:40

では、キーボードの「→」キーが押されたときに四角を右に1マス動かすにはどうするか。

まずは「→」キーが押されたときに画面に変化を出すことからはじめる。たとえば「→」キーを押すと四角を1つ描くとしよう。

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	if(key == 39){
		drawRect(5,5); // 5の5の位置に四角を描く
	}
}

次はキーを押したときに四角が動くようにしたい。

Sunabaではキーが押されたときに特定のメモリの値が変わるので、そのメモリを使って四角を描く位置を変えていた。そして、いつキーが押されても四角を描けるように、無限ループを用意し、その中で、四角を描いて、消すプログラムを書けばよかった。

しかし、押されたキーをイベントリスナーを使って取得する今回の方法では、無限ループの中でどのキーが押されたのか判定することができない。代わりに、handleKeyUp()関数が実行されるたびに、四角を描いて、消すことをする。

drawRect(5, 5); //  5の5の位置に四角を描いておく

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	if(key == 39){// 右 
		eraseRect(5, 5);// 5の5の四角を消して
		drawRect(5, 6); // 5の6の四角を描く
	}
}

右キーが押されたとき、それまであった四角を消し、ひとつ右の位置に四角を描けばよい。

var X = 0;
var Y = 0;
drawRect(Y, X); //  0の0の位置に四角を描いておく

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	if(key == 39){// 右 
		eraseRect(Y, X);
		drawRect(Y, X+1);
	}
}

右キーを押すと四角が0の0の位置から0の1の位置に動く。しかし、2回目に右キーを押しても四角はもう動かない。2回目にはX=2になってほしいが、X=0のままだからである。キーが押されるたびに、XとYの値は更新される必要がある。

var X = 0;
var Y = 0;
drawRect(Y, X); //  0の0の位置に四角を描いておく

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	if(key == 39){// 右 
		eraseRect(Y, X);
		X = X + 1;
		drawRect(Y, X);
	}
}

ほかのキーにでも動くようにする。

var X = 0;
var Y = 0;
drawRect(Y, X); //  0の0の位置に四角を描いておく

function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	if(key == 39){// 右 
		eraseRect(Y, X);
		X = X + 1;
		drawRect(Y, X);
	}
	if(key == 37){// 左 
		eraseRect(Y, X);
		X = X - 1;
		drawRect(Y, X);
	}
	if(key == 38){// 上 
		eraseRect(Y, X);
		Y = Y - 1;
		drawRect(Y, X);
	}
	if(key == 40){// 下 
		eraseRect(Y, X);
		Y = Y + 1;
		drawRect(Y, X);
	}
}

次は落ちてくる四角を動かせるようにする。前回書いた、四角を落とす関数はこうだった。

/* 縦y横xの位置から、hマスだけ、tミリ秒/マスの速さで四角を落とす。*/
function dropRect(y, x, h, t){
	var y0 = y
	var drop = setInterval(function(){
		if(y > 0) eraseRect(y-1, x);
		drawRect(y, x);
		y += 1;
		if(y > y0 + h) clearInterval(drop);
	}, t);
}

下のように、y = Y, x = XとしてdropRect関数を呼び出すだけでは上手くいかない。

var X = 0;
var Y = 0;
drawRect(Y, X); //  0の0の位置に四角を描いておく
dropRect(Y, X, 20, 500);

0,0の位置から四角は落ちていくが、動かせる四角は0,0の位置にある別の四角となる。dropRect関数の中のy,xはY,Xとは別物である。

四角が動くのは、押されたキーに応じてXとYが変化するからである。値を変える前に、それまでにあった四角を消し、キーに応じて値を変え、改めて四角を描く。

同じように、その四角が自動的に落ちていくようにするには、「四角を消し、Yの値を増やし、四角を描く」これを一定間隔で繰り返すようにすればよい。dropRect関数を次のように書き換える。

function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		Y += 1;
		drawRect(Y, X);
	}, t);
}

clearIntervalは省いているので、この関数は延々と繰り返す。Yが大きくなって領域外の値になるとエラーになる。もちろん、キーを押して四角を領域外に移動させてもエラーになる。これらの問題は後で対応する。ついでにhandleKeyUp関数を少しすっきりさせた。

var X = 0;
var Y = 0;
drawRect(Y, X); //  0の0の位置に四角を描いておく
dropRect(500);


function dropRect(t){
	var drop = setInterval(function(){
		eraseRect(Y, X);
		Y += 1;
		drawRect(Y, X);
	}, t);
}


function handleKeyUp(e){
	if(typeof e == 'undefined') e = window.event;
	var key = e.keyCode;
	
	eraseRect(Y, X);

	if(key == 39){// 右 
		X = X + 1;
	}
	if(key == 37){// 左 
		X = X - 1;
	}
	if(key == 38){// 上 
		Y = Y - 1;
	}
	if(key == 40){// 下 
		Y = Y + 1;
	}
	
	drawRect(Y, X);
	
}

7. ここまでのまとめ

本書の第7章はメモリに名前をつける、すなわち変数の紹介だった。このブログではJavaScriptで最初から変数を使って書いているので、今回はプログラムの進展はほぼない。

前回作った、四角を落とすプログラムを関数化してみる。

/* 四角を落とす */
var y = 0;
var drop = setInterval(function(){
	if(y > 0) eraseRect(y-1, 5);
	drawRect(y, 5);
	y += 1;
	if(y > 18) clearInterval(drop);
},500);

縦y横xの位置から、hマスだけ、tミリ秒/マスの速さで四角を落とす。

function dropRect(y, x, h, t){
	var y0 = y
	var drop = setInterval(function(){
		if(y > 0) eraseRect(y-1, x);
		drawRect(y, x);
		y += 1;
		if(y > y0 + h) clearInterval(drop);
	}, t);
}

前回省略した壁と底を描くプログラムと合体させた。