静岡理工科大学 菅沼ホーム JavaScript 目次 索引

8パズル

  1. ステップ1: ゲームの枠組み

      8パズルでは,3 × 3 のマス目に 1 ~ 8 の表示がある 8 つのコマが配置され,1 つのマス目にはコマが置かれていません.このとき,空白のマス目にコマを移動する操作を繰り返すことによって,初期状態から目標状態を,できるだけ少ないコマの移動によって達成するゲームです.例えば,下に示す左の状態を,コマの移動によって右の状態に持って行くことになります.

      

      基本的に,「ゲーム枠の作成」で説明した方法とほぼ同じ方法で作成します.ただし,画面のサイズは変更しています.また,ゲームオーバーの画面は存在しません.以下,各プログラムに対して,「ゲーム枠の作成」の場合との違いについて説明していきます.

    1. HTML ファイル

        「ゲーム枠の作成」における HTML ファイルとほとんど同じですが,「ゲームクリア」ボタンと「ゲームオーバー」ボタンは除いてあります.

      <!DOCTYPE HTML>
      <HTML>
      <HEAD>
      	<TITLE>8パズル:ステップ1</TITLE>
      	<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=utf-8">
      	<LINK REL="stylesheet" TYPE="text/css" HREF="../../../master.css">
      	<SCRIPT TYPE="text/javascript" SRC="main/MainPanel.js"></SCRIPT>
      	<SCRIPT TYPE="text/javascript" SRC="start/StartPanel.js"></SCRIPT>
      	<SCRIPT TYPE="text/javascript" SRC="game/GamePanel.js"></SCRIPT>
      	<SCRIPT TYPE="text/javascript" SRC="clear/GameClearPanel.js"></SCRIPT>
      </HEAD>
      <BODY CLASS="eeffee" onLoad="mp_start()">
      	<H1>8パズル:ステップ1</H1>
      	<CANVAS ID="canvas_e" STYLE="background-color: #ffffff;" WIDTH="190" HEIGHT="190"></CANVAS><BR>
      	<A HREF="method.htm" TARGET="method"><BUTTON ID="method" CLASS="std">遊び方</BUTTON></A>
      	<BUTTON ID="start" CLASS="std" onClick="gp_start()">ゲーム開始</BUTTON>
      	<BUTTON ID="first" CLASS="std" onClick="st_start()">最初から再開</BUTTON>
      	<BUTTON ID="finish" CLASS="std" onClick="mp.finish()">ゲーム終了</BUTTON>
      </BODY>
      </HTML>
      				

    2. MainPanel

        このプログラムに関しても,「ゲーム枠の作成」における MainPanel とほとんど同じです.ボタンの制御部分が異なっているだけです.

      mp = null;   // MainPanel オブジェクト
      
      			//
      			// MainPanel の開始
      			//
      function mp_start()
      {
      					// キャンバス情報
      	var canvas = document.getElementById('canvas_e');   // キャンバス要素の取得
      	var ctx    = canvas.getContext('2d');   // キャンバスからコンテキストを取得
      					// MainPanel オブジェクト
      	mp = new MainPanel(canvas, ctx);
      					// StartPanel の表示
      	st_start();
      }
      			//
      			// MainPanel オブジェクト(プロパティ)
      			//
      function MainPanel(canvas, ctx)
      {
      	this.canvas = canvas;   // キャンバス要素
      	this.ctx    = ctx;   // キャンバスのコンテキスト
      	this.level  = 1;   // ゲームレベル
      	return this;
      }
      			//
      			// MainPanel オブジェクト(メソッド)
      			//
      MainPanel.prototype.finish = function()
      {
      					// キャンバスのクリア
      	mp.ctx.clearRect(0, 0, mp.canvas.width, mp.canvas.height);
      					// ボタンを非表示
      	document.getElementById('method').style.display = "none";
      	document.getElementById('start').style.display = "none";
      	document.getElementById('first').style.display = "none";
      	document.getElementById('finish').style.display = "none";
      }
      				

    3. StartPanel

        このプログラムに関しても,「ゲーム枠の作成」における StartPanel とほとんど同じです.ボタンの制御部分が異なっているだけです.当然のことながら,ゲームタイトル及び「遊び方」の内容を変更しています.

      			//
      			// StartPanel の開始
      			//
      function st_start()
      {
      	mp.level = 1;   // ゲームレベルの設定
      					// キャンバスのクリア
      	mp.ctx.clearRect(0, 0, mp.canvas.width, mp.canvas.height);
      					// ゲームタイトルの表示
      	mp.ctx.font = "40px 'MS ゴシック'";
      	mp.ctx.textBaseline = "middle";
      	mp.ctx.textAlign = "center";
      	mp.ctx.fillStyle = "rgb(0, 0, 0)";
      	mp.ctx.fillText("8パズル", mp.canvas.width/2, mp.canvas.height/2);
      					// ボタンの表示制御
      	document.getElementById('method').style.display = "";
      	document.getElementById('start').style.display = "";
      	document.getElementById('first').style.display = "none";
      	document.getElementById('finish').style.display = "none";
      	document.getElementById('start').innerHTML = "ゲーム開始";
      }
      				

    4. GamePanel

        GamePanel は,実際のゲームを実現する部分です.従って,「ゲーム枠の作成」における GamePanel とは,ゲームの種類によってその内容は大きく異なります.今後,このプログラムを完成させていくことになりますが,ここでは,目標状態を表示しています.

      01	gp = null;   // GamePanel オブジェクト
      02	
      03				//
      04				// GamePanel の開始
      05				//
      06	function gp_start()
      07	{
      08						// GamePanel オブジェクト
      09		gp = new GamePanel();
      10						// 描画
      11		var timerID = setTimeout("gp.draw()", 100);
      12						// ボタンの表示制御
      13		document.getElementById('method').style.display = "none";
      14		document.getElementById('start').style.display = "none";
      15		document.getElementById('first').style.display = "none";
      16		document.getElementById('finish').style.display = "none";
      17	}
      18				//
      19				// GamePanel オブジェクト(プロパティ)
      20				//
      21	function GamePanel()
      22	{
      23		this.sz = 50;   // コマの大きさ
      24		this.gap = 10;   // コマ間のギャップ
      25		this.i_state = new Array();   // 盤面の初期状態
      26		this.i_state[0] = new Array(1, 2, 3);
      27		this.i_state[1] = new Array(8, 0, 4);
      28		this.i_state[2] = new Array(7, 6, 5);
      29		this.g_state = new Array();   // 盤面の目標状態
      30		this.g_state[0] = new Array(1, 2, 3);
      31		this.g_state[1] = new Array(8, 0, 4);
      32		this.g_state[2] = new Array(7, 6, 5);
      33		this.img = new Array;   // コマの画像の読み込み
      34		for (var i1 = 1; i1 <= 8; i1++) {
      35			this.img[i1-1] = new Image();
      36			this.img[i1-1].src = "image/" + i1 + ".gif";
      37		}
      38		return this;
      39	}
      40				//
      41				// GamePanel オブジェクト(メソッド draw)
      42				//
      43	GamePanel.prototype.draw = function()
      44	{
      45						// キャンバスのクリア
      46		mp.ctx.clearRect(0, 0, mp.canvas.width, mp.canvas.height);
      47						// 描画
      48		for (var i1 = 0; i1 < 3; i1++) {
      49			for (var i2 = 0; i2 < 3; i2++) {
      50				var k = gp.i_state[i1][i2] - 1;
      51				if (k >= 0)
      52					mp.ctx.drawImage(gp.img[k], gp.gap+i2*(gp.gap+gp.sz), gp.gap+i1*(gp.gap+gp.sz));
      53			}
      54		}
      55	}
      				
      09 行目( gp_start 関数)

        GamePanel オブジェクトを生成し,その結果をグローバル変数 gp( 01 行目)に代入しています.具体的には,21 行目~ 39 行目に記述された GamePanel 関数が実行されます.

      11 行目( gp_start 関数)

        描画しています.単に,「 gp.draw() 」だと描画してくれません.恐らく,画像読み込みにかかる時間のためだと思います.なお,GamePanel オブジェクトのメソッド draw は,43 行目~ 55 行目に定義してあります.

      25 行目~ 32 行目( GamePanel 関数)

        ゲームの初期状態( i_state,コマの移動によって変化する)と目標状態( g_state )を入れる 2 次元配列の定義と初期設定です.ここで示すように,2 次元配列は,まず配列を定義し( 25,29 行目),その配列の各要素を再び配列として定義する( 26 行目~ 28 行目,30 行目~ 32 行目)ことによって可能です.

        一般に,配列の各要素を配列として定義することによって,多次元配列を定義することが可能です.以下に示すのは 2 行 3 列の 2 次元配列の例です.
        var a = new Array(2);   // var a = new Array(); でも可
        for (var i1 = 0; i1 < 2; i1++)
        	a[i1] = new Array(3);
        					
      初期設定も同時に行いたい場合は,例えば,以下のようにして行います.
        var a = new Array(2);   // var a = new Array(); でも可
        a[0] = new Array(1, 2, 3);
        a[1] = new Array(4, 5, 6);
        					
      48 行目~ 54 行目( draw メソッド)

        i_state の値を調べ,0 以外の場所に対応する画像を描画しています.

    5. GameClearPanel

        このプログラムに関しても,「ゲーム枠の作成」における GameClearPanel とほとんど同じです.違いは,ボタンの制御部分と,レベルが 2 までしか無い点だけです.

      			//
      			// GameClearPanel の開始
      			//
      function gcp_start()
      {
      					// キャンバスのクリア
      	mp.ctx.clearRect(0, 0, mp.canvas.width, mp.canvas.height);
      					// タイトルの表示
      	mp.ctx.font = "40px 'MS ゴシック'";
      	mp.ctx.textBaseline = "middle";
      	mp.ctx.textAlign = "center";
      	mp.ctx.fillStyle = "rgb(0, 0, 0)";
      	mp.ctx.fillText("Game", mp.canvas.width/2, mp.canvas.height/2-20);
      	mp.ctx.fillText("Clear!", mp.canvas.width/2, mp.canvas.height/2+20);
      					// ボタンの表示制御
      	document.getElementById('method').style.display = "none";
      	if (mp.level > 1) {   // 最初からゲーム再開
      		document.getElementById('start').style.display = "none";
      		document.getElementById('first').style.display = "";
      	}
      	else {   // レベルアップ
      		mp.level++;
      		document.getElementById('start').style.display = "";
      		document.getElementById('first').style.display = "none";
      		document.getElementById('start').innerHTML = "次のレベル";
      	}
      	document.getElementById('finish').style.display = "";
      }
      				

  2. ステップ2: 初期状態の生成

      ここでは,初期状態を生成し,その状態をボタンに表示しています.GamePanel の gp_start 関数を修正すると共に,初期状態を生成するメソッド create を GamePanel オブジェクトに追加しています.

    001	gp = null;   // GamePanel オブジェクト
    002	
    003				//
    004				// GamePanel の開始
    005				//
    006	function gp_start()
    007	{
    008						// GamePanel オブジェクト
    009		gp = new GamePanel();
    010						// 初期状態の生成
    011		gp.create();
    012						// 描画
    013		var timerID = setTimeout("gp.draw()", 100);
    014						// ボタンの表示制御
    015		document.getElementById('method').style.display = "none";
    016		document.getElementById('start').style.display = "none";
    017		document.getElementById('first').style.display = "none";
    018		document.getElementById('finish').style.display = "none";
    019	}
    020				//
    021				// GamePanel オブジェクト(プロパティ)
    022				//
    023	function GamePanel()
    024	{
    025		this.sz = 50;   // コマの大きさ
    026		this.gap = 10;   // コマ間のギャップ
    027		this.i_state = new Array();   // 盤面の初期状態
    028		this.i_state[0] = new Array(1, 2, 3);
    029		this.i_state[1] = new Array(8, 0, 4);
    030		this.i_state[2] = new Array(7, 6, 5);
    031		this.g_state = new Array();   // 盤面の目標状態
    032		this.g_state[0] = new Array(1, 2, 3);
    033		this.g_state[1] = new Array(8, 0, 4);
    034		this.g_state[2] = new Array(7, 6, 5);
    035		this.img = new Array;   // コマの画像の読み込み
    036		for (var i1 = 1; i1 <= 8; i1++) {
    037			this.img[i1-1] = new Image();
    038			this.img[i1-1].src = "image/" + i1 + ".gif";
    039		}
    040		return this;
    041	}
    042				//
    043				// GamePanel オブジェクト(メソッド create)
    044				//
    045	GamePanel.prototype.create = function()
    046	{
    047		var ct = (mp.level == 1) ? 10 : 100;
    048		var sw = true;
    049	
    050		while (sw) {
    051			var k1 = 1;
    052			var k2 = 1;
    053						// 移動
    054			for (var i1 = 0; i1 < ct; i1++) {
    055				var dr = Math.floor(4 * Math.random());
    056				switch (dr) {
    057					case 0:   // 上
    058						if (k1 > 0) {
    059							wk = gp.i_state[k1-1][k2];
    060							gp.i_state[k1-1][k2] = 0;
    061							gp.i_state[k1][k2]   = wk;
    062							k1--;
    063						}
    064						break;
    065					case 1:   // 下
    066						if (k1 < 2) {
    067							wk = gp.i_state[k1+1][k2];
    068							gp.i_state[k1+1][k2] = 0;
    069							gp.i_state[k1][k2]   = wk;
    070							k1++;
    071						}
    072						break;
    073					case 2:   // 左
    074						if (k2 > 0) {
    075							wk = gp.i_state[k1][k2-1];
    076							gp.i_state[k1][k2-1] = 0;
    077							gp.i_state[k1][k2]   = wk;
    078							k2--;
    079						}
    080						break;
    081					default:   // 右
    082						if (k2 < 2) {
    083							wk = gp.i_state[k1][k2+1];
    084							gp.i_state[k1][k2+1] = 0;
    085							gp.i_state[k1][k2]   = wk;
    086							k2++;
    087						}
    088						break;
    089				}
    090			}
    091						// ゴールと同じか否かのチェック
    092			for (var i1 = 0; i1 < 3 && sw; i1++) {
    093				for (var i2 = 0; i2 < 3 && sw; i2++) {
    094					if (gp.i_state[i1][i2] != gp.g_state[i1][i2])
    095						sw = false;
    096				}
    097			}
    098		}
    099	}
    100				//
    101				// GamePanel オブジェクト(メソッド draw)
    102				//
    103	GamePanel.prototype.draw = function()
    104	{
    105						// キャンバスのクリア
    106		mp.ctx.clearRect(0, 0, mp.canvas.width, mp.canvas.height);
    107						// 描画
    108		for (var i1 = 0; i1 < 3; i1++) {
    109			for (var i2 = 0; i2 < 3; i2++) {
    110				var k = gp.i_state[i1][i2] - 1;
    111				if (k >= 0)
    112					mp.ctx.drawImage(gp.img[k], gp.gap+i2*(gp.gap+gp.sz), gp.gap+i1*(gp.gap+gp.sz));
    113			}
    114		}
    115	}
    			
    011 行目( gp_start 関数)

      GamePanel オブジェクトのメソッド create( 045 行目~ 099 行目)を呼び,初期状態を生成しています.

    045 行目~ 099 行目( create メソッド)

      任意の初期状態から,コマの移動によって目標状態に到達できるとは限らないため,逆に,目標状態からランダムに ct 回コマを移動させ初期状態を生成しています.ただし,その結果が偶然目標状態と一致する可能性も存在するため,092 行目~ 097 行目において,生成された初期状態が目標状態と一致しないことを確認しています.これが,050 行目の while 文を使用している理由です.

      047 行目の ? : は,条件演算子と呼ばれ,一般的には以下のように記述されます.論理式が評価され,その結果が真であれば,式1を評価した結果が変数に代入され,偽であれば,式2を評価した結果が変数に代入されます.
      変数 = (論理式) ? 式1 : 式2
       				
    従って,047 行目は,レベルが 1 であれば,ct の値を 10 とし,そうでなければ,100 とするという意味になります.

      switch 文( 056 行目~ 089 行目)の一般形式は以下の通りであり,式の値が定数式に一致した case 文以下が実行されます.いずれの定数式とも一致しなかった場合は,default 以下が実行されます.なお,break 文は,強制的に switch 文の外に出るための文であり,外へ出た後,switch 文の後の文から実行されます.
      switch (式) {
      	[case 定数式1 : ]
      		[文1]
      	[case 定数式2 : ]
      		[文2]
      	 ・・・・・
      	[default : ]
      		[文n]
      }
      				

  3. ステップ3: 完成

      ここでは,ボタンに対して,クリックすると移動する機能を加え,ゲームを完成します.GamePanel の gp_start 関数を修正すると共に,コマがクリックされた時の処理を実行するメソッド mouseClick を GamePanel オブジェクトに追加しています.

    001	gp = null;   // GamePanel オブジェクト
    002	
    003				//
    004				// GamePanel の開始
    005				//
    006	function gp_start()
    007	{
    008						// GamePanel オブジェクト
    009		gp = new GamePanel();
    010						// 初期状態の生成
    011		gp.create();
    012						// 描画
    013		var timerID = setTimeout("gp.draw()", 100);
    014						// マウスクリックに対するイベントリスナ
    015		mp.canvas.addEventListener("click", gp.mouseClick);
    016						// ボタンの表示制御
    017		document.getElementById('method').style.display = "none";
    018		document.getElementById('start').style.display = "none";
    019		document.getElementById('first').style.display = "none";
    020		document.getElementById('finish').style.display = "none";
    021	}
    022				//
    023				// GamePanel オブジェクト(プロパティ)
    024				//
    025	function GamePanel()
    026	{
    027		this.sz = 50;   // コマの大きさ
    028		this.gap = 10;   // コマ間のギャップ
    029		this.i_state = new Array();   // 盤面の初期状態
    030		this.i_state[0] = new Array(1, 2, 3);
    031		this.i_state[1] = new Array(8, 0, 4);
    032		this.i_state[2] = new Array(7, 6, 5);
    033		this.g_state = new Array();   // 盤面の目標状態
    034		this.g_state[0] = new Array(1, 2, 3);
    035		this.g_state[1] = new Array(8, 0, 4);
    036		this.g_state[2] = new Array(7, 6, 5);
    037		this.img = new Array;   // コマの画像の読み込み
    038		for (var i1 = 1; i1 <= 8; i1++) {
    039			this.img[i1-1] = new Image();
    040			this.img[i1-1].src = "image/" + i1 + ".gif";
    041		}
    042		return this;
    043	}
    044				//
    045				// GamePanel オブジェクト(メソッド mouseClick)
    046				//
    047	GamePanel.prototype.mouseClick = function(event)
    048	{
    049		var x_base  = mp.canvas.offsetLeft;   // キャンバスの左上のx座標
    050		var y_base  = mp.canvas.offsetTop;   // キャンバスの左上のy座標
    051		var x       = event.pageX - x_base;    // キャンバス内のクリックされた位置(x座標)
    052		var y       = event.pageY - y_base;    // キャンバス内のクリックされた位置(y座標)
    053						// クリックされたコマ
    054		var k1 = -1;
    055		var k2 = -1;
    056		for (var i1 = 0; i1 < 3; i1++) {
    057			if (y >= gp.gap+i1*(gp.gap+gp.sz) && y <= (i1+1)*(gp.gap+gp.sz)) {
    058				k1 = i1;
    059				break;
    060			}
    061		}
    062		for (var i1 = 0; i1 < 3; i1++) {
    063			if (x >= gp.gap+i1*(gp.gap+gp.sz) && x <= (i1+1)*(gp.gap+gp.sz)) {
    064				k2 = i1;
    065				break;
    066			}
    067		}
    068						// コマを移動
    069		if (k1 >= 0 && k2 >= 0 && gp.i_state[k1][k2] > 0) {
    070			var sw = false;
    071			if (k1 > 0 && gp.i_state[k1-1][k2] == 0) {   // 上
    072				sw = true;
    073				gp.i_state[k1-1][k2] = gp.i_state[k1][k2];
    074				gp.i_state[k1][k2]   = 0;
    075			}
    076			else if (k1 < 2 && gp.i_state[k1+1][k2] == 0) {   // 下
    077				sw = true;
    078				gp.i_state[k1+1][k2] = gp.i_state[k1][k2];
    079				gp.i_state[k1][k2]   = 0;
    080			}
    081			else if (k2 > 0 && gp.i_state[k1][k2-1] == 0) {   // 左
    082				sw = true;
    083				gp.i_state[k1][k2-1] = gp.i_state[k1][k2];
    084				gp.i_state[k1][k2]   = 0;
    085			}
    086			else if (k2 < 2 && gp.i_state[k1][k2+1] == 0) {   // 右
    087				sw = true;
    088				gp.i_state[k1][k2+1] = gp.i_state[k1][k2];
    089				gp.i_state[k1][k2]   = 0;
    090			}
    091								// ゴールか?
    092			if (sw) {
    093				gp.draw();
    094				for (var i1 = 0; i1 < 3 && sw; i1++) {
    095					for (var i2 = 0; i2 < 3 && sw; i2++) {
    096						if (gp.i_state[i1][i2] != gp.g_state[i1][i2])
    097							sw = false;
    098					}
    099				}
    100				if (sw)
    101					gcp_start();   // ゲームクリア
    102			}
    103		}
    104	}
    105				//
    106				// GamePanel オブジェクト(メソッド create)
    107				//
    108	GamePanel.prototype.create = function()
    109	{
    110		var ct = (mp.level == 1) ? 10 : 100;
    111		var sw = true;
    112	
    113		while (sw) {
    114			var k1 = 1;
    115			var k2 = 1;
    116						// 移動
    117			for (var i1 = 0; i1 < ct; i1++) {
    118				var dr = Math.floor(4 * Math.random());
    119				switch (dr) {
    120					case 0:   // 上
    121						if (k1 > 0) {
    122							wk = gp.i_state[k1-1][k2];
    123							gp.i_state[k1-1][k2] = 0;
    124							gp.i_state[k1][k2]   = wk;
    125							k1--;
    126						}
    127						break;
    128					case 1:   // 下
    129						if (k1 < 2) {
    130							wk = gp.i_state[k1+1][k2];
    131							gp.i_state[k1+1][k2] = 0;
    132							gp.i_state[k1][k2]   = wk;
    133							k1++;
    134						}
    135						break;
    136					case 2:   // 左
    137						if (k2 > 0) {
    138							wk = gp.i_state[k1][k2-1];
    139							gp.i_state[k1][k2-1] = 0;
    140							gp.i_state[k1][k2]   = wk;
    141							k2--;
    142						}
    143						break;
    144					default:   // 右
    145						if (k2 < 2) {
    146							wk = gp.i_state[k1][k2+1];
    147							gp.i_state[k1][k2+1] = 0;
    148							gp.i_state[k1][k2]   = wk;
    149							k2++;
    150						}
    151						break;
    152				}
    153			}
    154						// ゴールと同じか否かのチェック
    155			for (var i1 = 0; i1 < 3 && sw; i1++) {
    156				for (var i2 = 0; i2 < 3 && sw; i2++) {
    157					if (gp.i_state[i1][i2] != gp.g_state[i1][i2])
    158						sw = false;
    159				}
    160			}
    161		}
    162	}
    163				//
    164				// GamePanel オブジェクト(メソッド draw)
    165				//
    166	GamePanel.prototype.draw = function()
    167	{
    168						// キャンバスのクリア
    169		mp.ctx.clearRect(0, 0, mp.canvas.width, mp.canvas.height);
    170						// 描画
    171		for (var i1 = 0; i1 < 3; i1++) {
    172			for (var i2 = 0; i2 < 3; i2++) {
    173				var k = gp.i_state[i1][i2] - 1;
    174				if (k >= 0)
    175					mp.ctx.drawImage(gp.img[k], gp.gap+i2*(gp.gap+gp.sz), gp.gap+i1*(gp.gap+gp.sz));
    176			}
    177		}
    178	}
    			
    015 行目( gp_start 関数)

      マウスのクリックに対応するイベントリスナを追加しています.この結果,キャンバス上でマウスをクリックすると, GamePanel オブジェクトの mouseClick メソッド( 047 行目~ 104 行目)が実行されます.

    054 行目~ 067 行目( mouseClick メソッド)

      どのコマ( k1 行 k2 列のコマ)がクリックされたかを調べています.for 文の内部で使用されている break 文( 059,065 行目)は,繰り返しの終了などに使用される文であり,この例の場合,break 文が実行されると,繰り返しを終了し,for 文の外に出ます( for 文の次の文が実行される).

    070 行目~ 090 行目( mouseClick メソッド)

      コマを移動できる場合は,コマを移動しています.

    094 行目~ 099 行目( mouseClick メソッド)

      目標状態に達したか否かを調べています.目標状態になった場合は,ゲームクリアとなります( 101 行目).

静岡理工科大学 菅沼ホーム JavaScript 目次 索引