【enchant.js】神経衰弱の作り方【TCard.js】

今回は先日公開したTCard.jsを使って、実際にゲームを作ってみたいと思います。
作るゲームは一人用神経衰弱です。
enchant.jsを使用しますが、enchant.jsの解説は行いませんのでご注意下さい。

みなさん御存知の通り、神経衰弱とは伏せた状態で場に置かれたカードの内2枚を選び、同じ数であればそのカードを得ることが出来るというゲームです。本来は多人数用ゲームで得たカードの数を競うのですが、今回は一人用ということで選んだカードが異なる数でもプレイを続行できるというようにしたいと思います。
それではゲームの流れを見てみましょう。
0.カードの準備
1.カードを場に広げる
2.カードの選択
2-1.1枚目のカードを選択
2-2.2枚目のカードを選択
3.判定
3-1.同じ数なら場からのぞいて得点札に
3-2.異なる数ならカードを裏返す
4.終了判定
4-1.場にカードがないなら終了
4-2.場にカードが有るなら2に戻る

それではこの流れに沿って、実際にコードを書いていきたいと思います。

0.カードの準備

まずindex.htmlです。

<!doctype html>
<html>
<head>
    <link rel='stylesheet' href='style.css' type='text/css'>
    <script src='/static/enchant.js-latest/enchant.js'></script>
    <script src='https://rawgithub.com/v416/TCard.js/master/TCard.js'></script>
    <!-- TCard.js 及び enchant.js を読み込んだ後に TCard.enchant.js を読み込んで下さい -->
    <script src='https://rawgithub.com/v416/TCard.js/master/TCard.enchant.js'></script>
    <script src='/code.9leap.js'></script>
    <script src='main.js'></script>
    <title>Page title</title>
</head>
<body>
</body>
</html>

TCard.jsとenchant.jsはどちらを先に読み込んでも構いません。TCard.enchant.jsは先の2ファイルに依存しているので、後に読み込んでください。

次はmain.jsです。

enchant();
window.onload = function(){
    var game = new Core(320, 320);
    // TCard.enchant.js で必要とする画像です
    game.preload("icon0.png", "font1.png", "enchant.png");
    game.fps = 15;
    game.onload = function(){
        // カードの画像を assets に登録します
        enchant.TCard.setPack();
    };
    game.start();
};

preloadでicon0.png、font1.png、enchant.pngの3ファイルを読み込んでいます。この3ファイルはenchant.jsに付属する画像ファイルで、TCard.enchant.jsはこれらを使ってトランプの画像を作成します。icon0.pngのスートとfont1.pngの数字・英字でカードの表を、enchant.pngでカードの裏を作成しています。いずれもMITライセンスのフリー素材ですが、オリジナルゲームを作成する際にenchant.pngをそのまま使うのは憚れると思います。もし変更したい場合は512*512の画像をenchant.pngという名前で用意して差し替えて下さい。

game.onload内でenchant.TCard.setPackという関数を実行します。この関数でトランプ画像を作成、game.assets[enchant.TCard.CARDS]に登録しています。enchant.TCard.getCardを使うと、1枚のカードをSpriteで取得することができます。このカードは以下の様な仕様となっています。
・縦幅:48pixel
・横幅:48pixel
・frame=0:カードの表
・frame=1:カードの裏
・dataプロパティ:カードの通し番号
1セット取得する場合はenchant.TCard.getDeckを使います。

これでカードの準備、TCard.jsとTCard.enchant.jsを利用する際の『お約束』は整いました。
テンプレートをこちらに用意してありますので、DL、もしくはForkしてお使いください。

1.カードを場に広げる

まずはカードを1セット分取得しましょう。

var deck = enchant.TCard.getDeck();

この関数で1セット分、つまり52枚のカードが取得できることは先に述べました。返ってくるのはGroupでまとめられたSpriteです。ジョーカーも欲しい場合は、引数にほしいジョーカーの数を与えます。例えば

var deck = enchant.TCard.getDeck(2);

とした場合、ジョーカー2枚を含む54枚のカードがGroupにまとめられて返ってきます。

次はこのカードを混ぜます。
今回、私は以下の手順でカードを混ぜることにしました。(楽だから)
1.カードすべての枚数を取得し、変数lenに格納
2.lenを最大値とした乱数を取得し、変数iに格納
3.変数iの位置にあるカードをremoveChildし、GroupにaddChildしなおす
4.変数lenから1を引く
5.変数lenが0になるまで2に戻る

var len = deck.childNodes.length;
while (0 < len) {
    var i = ~~(Math.random() * len);
    var c = deck.childNodes[i];
    c.opacity = 0;
    deck.removeChild(c);
    deck.addChild(c);
    len--;
}
game.rootScene.addChild(deck);

Groupは突き詰めると配列なのですが、単純に配列をいじるだけでは表示順が変わりません。ですのできちっとremoveChild&addChildしましょう。ループの中でopacityを0にしているのは、今後の工程がプレイヤーに見えないようにするためです。最後にSceneにdeckをaddChildするのを忘れずに。


さて。いよいよ場にカードを配ります。
まずはカードを1枚づつ、ランダムな場所にランダムな角度で配置し、それを記録していきます。

var map = [];
deck.childNodes.forEach(function(card, index, others) {
    card.x = ~~(Math.random() * (320 - 48));
    card.y = ~~(Math.random() * (320 - 48));
    card.rotate(~~(Math.random() * 360));
    map[map.length] = {x:card.x, y:card.y, r:card.rotation};
});

forEachJavascriptのArrayに用意されている関数で、引数で与えた関数を配列の各要素に一度づつ実行してくれます。
/forEach
これだとカードが重なってしまうので重ならないように調整します。
あと、320*320のフィールドに48*48のSprite52枚はキャパシティ・オーバーなので、Spriteにサイズを小さくします。

var map = [];
deck.childNodes.forEach(function(card, index, others) {
    card.scale(0.7, 0.7);
    do {
        card.x = ~~(Math.random() * (320 - 40));
        card.y = ~~(Math.random() * (320 - 40));
        card.rotate(~~(Math.random() * 360));
    } while(others.some(function(other) {
                    return card.data!= other.data && card.within(other, 33);
                }))
    map[map.length] = {x:card.x, y:card.y, r:card.rotation};
});

someは、与えられた関数にひとつでも合格する要素があるかどうかを、true/falseで返します。


これで配置する場所は決まったので、次にカードを実際に配るアクションを組みます。
カードを一度フィールドの外に配置し、TimeLineを使って一気にカードを配ります。

deck.childNodes.forEach(function(card, index) {
    card.x = 160 - 16;
    card.y = -34;
    card.rotate(0);
    c.opacity = 1;
    card.frame = enchant.TCard.CLOSE;
    card.tl.moveTo(map[index].x, map[index].y, 3).and().rotateTo(map[index].r, 3).then(function() {
        var e = new Event("deal_end");
        e.data = card.data;
        card.parentNode.dispatchEvent(e);
    });
});

カードに、配置についたらdeckオブジェクトにその旨を通知する仕組も組込みました。
deckにもそれに対応する関数を準備しておきましょう。

deck.deal= 0;
deck.addEventListener("deal_end", function() {
    this.deal++;
    if (this.deal=== 52) console.log("deal end!");
});

3.判定

trunが実行されるとカードのSpriteは親にその旨を通知する仕組みになっています。それを受けて判定を実行するようにしましょう。2枚のカードが同じ数だったら場から消します。違う数だったらカードを裏に返します。いきなりカードが消えたり裏返ったりするとプレイヤーもカードの確認ができないしテンポも悪いので、Timelineのdelayを使って若干の間を作っています。

deck.addEventListener(enchant.TCard.TURN_END, function(e) {
    var self = this;
    if (this.touch.length === 2) {
        var c1= this.getCard(this.touch[0])[0];
        var c2= this.getCard(this.touch[1])[0];
        if (TCard.getNumber(c1.data) === TCard.getNumber(c2.data)) {
            this.touch = [];
            this.tl.delay(8).then(function() {
                self.removeChild(c1);
                self.removeChild(c2);
            });
        } else {
            this.touch = [];
            this.tl.delay(8).then(function() {
                c1.turn();
                c2.turn();
            });
        }
    }
});

4.終了判定

終了判定は上述のコードの中に仕込みます。まぁdeckのchildNodesが0になったら終了の旨を表示すればいいわけで、今回はenchant.jsの公式素材の中にあるclear.pngを表示させることにしたいと思います。
まずはpreloadでclear.pngを読み込みます。

game.preload("icon0.png", "font1.png", "enchant.png", "clear.png");

そして実際の判定。

deck.addEventListener(enchant.TCard.TURN_END, function(e) {
    var self = this;
    if (this.touch.length === 2) {
        var c1= this.getCard(this.touch[0])[0];
        var c2= this.getCard(this.touch[1])[0];
        if (TCard.getNumber(c1.data) === TCard.getNumber(c2.data)) {
            this.touch = [];
            this.tl.delay(8).then(function() {
                self.removeChild(c1);
                self.removeChild(c2);
                if (self.childNodes.length === 0) {    
                    var clear = new Sprite(270, 48);
                    clear.image = game.assets["clear.png"];
                    clear.x = 25;
                    clear.y = 140;
                    game.rootScene.addChild(clear);
                }
            });
        } else {
            this.touch = [];
            this.tl.delay(8).then(function() {
                c1.turn();
                c2.turn();
            });
        }
    }
});

これにてひと通り終わりました。すべてのコードはこちらになります。
次回はこれを元に、得点機能やリトライ機能の追加など、ブラッシュアップに努めたいと思います。