2012年4月24日火曜日

HTML5でソリティアを作ってみた(第1回)


HTML5勉強の一区切りとしてカードゲームのソリティアを作ってみました。
まず、下の画面写真を見て頂くと解るように、iPhone3GSでも動くことを目的としています。
このため動作確認は iPhone3GS(iOS5.1)、IS03(Android2.2)、FireFox(11.0)、IE(9.0.8112.16421) で行っています。
その他の環境での動作は保障できませんので、あしからず。
※どうもiPod Touch(4世代)にiOS4 の環境だと動きが鈍い(ドラッグへの追従が遅い)ようです。
テストコードは以下のページにあります。

[Solitare Single](カードを1組52枚使っています)
[Solitare Double](カードを2組104枚使っています)

また全ソースを見たいという方もここから落としてください。
後ほど説明するリストクラスは [List.js] にあります。
Doubleの方は104枚カードがあるためか、どうも動作が重いような感じ。
ま、この辺りは今後の課題ですね。
それから、私自身HTML5やJavaScriptは超ど素人なため、とんでもないコードになっている可能性もあります。
ですので、万が一にも(間違えて)このコードを参考に何かを作って大問題が発生したとしても当方は一切責任を取れませんので、その点はご理解下さい。

それでは、何回かに分けて簡単に説明していこうと思います。
【1.実行画面】
 
上部にはコントロール用のメニューを配置してます。
「New」で新しいゲーム開始。
「End」でゲームを終了。
「Time 00:00」には経過時間を表示します。
このメニュー部分は普通にHTMLで表現しています。

メニューの下がゲームステージになり、この部分は1つの canvas となっています。

まず、左上の4つの枠が、カード種別毎に1から順に積み上げていく「組み札」エリアです。
その右側で3枚開いているのは更にその右側にある「山札」を開いたカードです。
(「山札」のカードは3枚一度に開くというルールにしています。)
これらの下に7列表示されているのが「場札」で、ここは赤、黒交互に数字の大きい方から小さい方へ順にカードを重ねていく事ができます。
組み札、山札、場札は私が便宜上勝手に命名した名前ですが、今後もこの呼び方で説明しますので覚えておいてください。
【2.HTML】
HTML部分については余り難しい所はありません。
<!DOCTYPE html>
<html lang="ja">
  <head>
    
    
    solitare s
    
    
    
    
    
    
    
    
  </head>
  <body>
    
    
</body> </html>

<head>の<meta>部分は基本的に Aptana Studio3 が自動的に作成してくれた物をつかっています。
追加したのは、
<meta name="viewport" content="width=320; user-scalable=no" />
の部分で、ここでコンテンツの幅を 320px に制限して、ユーザーがスケーリングを変更できないように指定しています。
つまり、たったこの1行でiPhone3GSに最適な幅で、ピンチインやピンチアウトで画面の拡大率が変わらないようになります。素晴らしい!

<style>部分ですが、ここで注意して頂きたいのは、全エレメントに対して
position : absolute;
で絶対座標にしてる事です。
マウスクリックやタップ位置を処理するのに都合が良かったのが理由です。
ただ、このためにメニューやキャンバスにも座標をセットしなければならなくなっているのでご注意を。
また、メニューは hover した際に文字色を白くし、右端の経過時間は緑色の文字に設定しています。

HTML の方はいたってシンプルです。
id="menu_bar"の部分がメニュー部分で、"New" で NewGame()を呼び出してカードを初期化し新規ゲームをスタートします。
また、"End" は EndGame() を呼び出し、全てのカードを削除してゲームを終了します。
NewGame(), EndGame()については後ほど説明を行う予定です。

そして、divエレメントの id="game_stage" 内がゲームエリアになります。
この中には "stage_canvas" と、 "off_canvas" という2つの canvas エレメントがあり、"stage_canvas" は実際に表示されるゲームステージ用です。
これに対し "off_canvas" はドラッグ中のもたつきを抑える目的で使用するオフスクリーン用なので、display: none; を指定して常時表示させません。
"off_canvas"の具体的な説明も後ほどドラッグ処理の中で行います。

さて、キャンバスを使う場合に忘れてはいけないのが、width と height を指定する事です。 もし指定しないとブラウザが持っているデフォルト値になってしまいますので必ず指定するようにしましょう。
【3.リストクラスを作成する】
次はデータ構造について説明しましょう。
まず大前提として各カードはリスト構造で管理する事とします。
リスト構造とは、1つのアイテムが前後のアイテムへのリンクを持った構造の事で、線を手繰るようにして次々とアイテムを辿って行くことができます。
この構造の便利な所は、ある所から切り離して、別のリストに繋げるといった事が、前後のアイテムへのリンクを書き換えるだけで行える事にあります。
これが配列だったら、切り離す所から後ろのデータを全て別の配列にコピーするという処理が必要になり大変です・・・よね?
まぁリスト構造で実装したのには単に私がリスト構造が好きだからという理由もあります・・・が、でもでも便利なんですよ、本当に。

さぁ!リストですが、汎用的に使えるよう List.js ファイルに別途作成しています。

コードは以下の通り。
// リストのアイテムクラス
var ListItem = function(data,prev,next){
  this.prev = prev;  // 1つ前のListItemの参照。
  this.next = next;  // 1つ後ろのListItemの参照。
  this.data = data;  // 実際のデータへの参照。
};
// リストクラス本体
var List = function(){
  this.top    = null;   // 先頭のアイテム(の参照)
  this.last   = null;   // 最後のアイテム(の参照)
  this.length = 0;      // アイテム数
};
// List class method
List.prototype = {
  // アイテムの追加メソッド
  add : function(data) {  // 最後尾に追加
    var item = new ListItem(data,this.last,null);
    if( this.last != null ){
      this.last.next = item;
      this.last = item;
    }
    else{
      this.top = item;
      this.last = item;
    }
    this.length++;
    return item;
  },
  // アイテムの挿入メソッド
  // basepos の前に挿入 basepos がnullなら最後に挿入
  insert : function(basepos, data){
    if( basepos != null ){
      var previtem = basepos.prev;
      var item = new ListItem(data,previtem,basepos);
      basepos.prev = item;
      if( previtem == null ){
        this.top = item;
      }
      else{
        previtem.next = item;
      }
      this.length++;
      return item;
    }
    else{
      return this.add(data);
    }
  },
  // アイテムの削除メソッド
  remove : function(pos){
    if( pos != null && this.isitem( pos ) ){
      // 指定されたアイテムがリストに存在するかをチェックする
      if( this.top == pos ){
        this.top = pos.next;
      }
      if( this.last == pos ){
        this.last = pos.prev;
      }
      if( pos.next != null ){
        pos.next.prev = pos.prev;
      }
      if( pos.prev != null ){
        pos.prev.next = pos.next;
      }
      pos.next = null;
      pos.prev = null;
      delete pos;
      this.length--;
      return true;
    }
    return false;
  },
  // すべてを削除
  remove_all : function(){
    var item = this.top;
    while( item != null ){
      var next = item.next;
      delete item;
      item = next;
    }
    this.top = null;
    this.last = null;
    this.length = 0;
  },
  isitem : function(pos){
    if(pos==null)  return false;
    var bFound = false;
    var itempos = this.get_top();
    while( itempos != null ){
      if( itempos == pos ){
        bFound = true;
        break;
      }
      itempos = this.get_next(itempos);
    }
    return bFound;
  },
  get_data : function(pos){
    if(pos!=null)  return pos.data;
    else           return null;
  },
  get_prev : function(pos){
    if(pos!=null)  return pos.prev;
    else           return null;
  },
  get_next : function(pos){
    if(pos!=null)  return pos.next;
    else           return null;
  },
  get_top : function(){
    return this.top;
  },
  get_last : function(){
    return this.last;
  },
  get_length : function(){
    return this.length;
  },
};

ListItem はリスト内で使うアイテムクラスで、前後のリストクラスオブジェクトの参照とデータオブジェクトの参照を持ちます。

List がリストクラス本体です。
クラスと書きましたが厳密にはクラスでは無いんですよね、JavaScriptの場合は。 prototype へメソッドを追加していく事でクラスのような動きをさせているだけだそうです。
この辺は詳しく説明できないので、興味のある方は調べてみてください。
それでは、各メソッドについての説明です。
【データアクセス用メソッド】
メソッド名説明
get_top()先頭のリストアイテムの参照を返します。
get_last()最後のリストアイテムの参照を返します。
get_next(pos)posの1つ後ろのリストアイテムの参照を返します。 posが最後の場合は null を返します。 posにはget_top()等で得られたリストアイテムを指定します。
get_prev(pos)posの1つ前のリストアイテムの参照を返します。 posが先頭の場合は null を返します。 posにはget_top()等で得られたリストアイテムを指定します。
get_data(pos)posで指定したリストアイテムからデータの参照を取得します。
get_length()リストが持っているアイテムの数を返します。
【データの追加用メソッド】
メソッド名説明
add(data)最後尾にdataで指定したデータを追加します。
insert(basepos, data)baseposで指定したリストアイテムの前にdataを挿入します。 basepos が null なら最後尾に追加します。
【データの削除用メソッド】
メソッド名説明
remove(pos)posで指定したアイテムを削除します。
remove_all()リスト内の全アイテムを削除し、リストを初期状態に戻します。

実はget_next(),get_prev()では pos を進めてデータを返すようにしたかったのですが、JavaScriptでは C++ のようなポインタのポインタが使えない(ようなのです)。
このため仕方なく次(前)のアイテムを返していて、データは get_data() で取得するようにしています。
連想配列で返す手もあったのですが、それもちょっと違うかなと思ったのでこういう仕様としました。
この他にも通常リストクラスに用意されているメソッドは多々ありますが、今回は必要な機能のみとなってまして。
その辺りもおいおい実装できればと思っています。

ちょっと長くなってきたので第1回はここまでとさせて頂いて、続きは第2回で。
下手な説明に長々とお付き合い頂いて本当にありがとうございました。

0 件のコメント:

コメントを投稿