JavaScript:パズルゲームで次に来るブロックを表示しておく

JavaScript

ステージの上方に、次に配置可能なブロックを表示しておく。
次に来るブロックは、ブロックを配置する際(ステージクリック)にランダムで設定するようになっている。

ソースコード

index.html

1<!DOCTYPE html>
2<html lang="ja">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>OTIMONO(09次のブロックを表示する)</title>
7    <style>
8        *{
9            margin: 0;
10            padding: 0;
11        }
12    </style>
13    <script src="main.js" type="text/javascript"></script>
14</head>
15<body>
16    <canvas id="canvas" width="512" height="640"></canvas>
17</body>
18</html>

main.js

1// 09次のブロックを表示する
2let isDebug = true;         // デバッグモードON/OFF
3let canvas = null;
4let g = null;
5let pos = {x:0, y:0};       // キャンバス上のマウス座標
6let dropSpeed = 3;          // 落下速度(10-->1秒, 1-->0.1秒)
7let timer = {start: 0, end: 0}; // 落下速度計測用
8 
9const BLOCK_SIZE = 64;                              // 1ブロックのサイズ(=パズルキャラのサイズ)64x64ピクセル
10const ROW_SIZE = 8;                                 // 行数
11const COLUMN_SIZE = 8;                              // 列数
12const BLOCK_LENGTH = ROW_SIZE * COLUMN_SIZE;        // ステージ全体のブロック長(行数×列数)
13 
14// ゲームの状態(game.stateで使用: Symbolの引数文字列は状態表示に利用)
15const TITLE = Symbol('title');      // タイトル画面(未使用)
16const WAITING = Symbol('waiting');  // ブロック配置待ち
17const SET_BLOCK = Symbol('set_block');  // ブロック配置
18const DROP = Symbol('drop');        // 落下中
19const CHECK_BLOCK = Symbol('check_block');  // 消せるブロックをチェック
20const DELETE_BLOCK = Symbol('delete_block');    // ブロックを消去
21 
22// ステージ背景色(市松模様)
23const BACKGROUND_COLOR1 = "pink";
24const BACKGROUND_COLOR2 = "skyblue";
25 
26// ゲーム全体の設定や情報
27let game = {
28    state: WAITING,     // ゲーム中の状態
29    hiscore: 0,         // 未使用
30    offsetY: 128,       // キャンバス左上からのステージ座標オフセット値(Y座標)
31};
32 
33let currentBlockType = 0;   // 配置するブロックタイプ(1~6)
34let nextBlockType = 0;  // 次に配置するブロックタイプ(1~6)
35 
36// ゲームに使う画像ファイル
37const image_files = ["space.png", "alien.png", "apple.png", "dog.png", "obake.png", "ultra.png", "usi.png", "delete.png"];
38 
39// 画像オブジェクト(画像ファイルをゲームで利用するため)
40let images = [];
41 
42// ステージ情報配列
43let stageState = new Array(BLOCK_LENGTH);
44 
45/*
46 * マウスを動かしている時の処理
47 */
48const mouseMoveListener = (e) => {
49    // オフセット位置:キャンバスがブラウザの左上からどれくらいの位置にあるか?(rect.left, rect.top)
50    const rect = e.target.getBoundingClientRect();
51 
52    // e.clientXとe.clientYはブラウザ左上からのキャンバスクリック位置なのでオフセット分を引くとキャンバス上の座標が分かる
53    pos.x = e.clientX - rect.left;
54    pos.y = e.clientY - rect.top;
55};
56 
57/*
58 * マウスボタンを押した時の処理
59 */
60const mouseDownListener = () => {
61    // ブロックが配置できる条件
62    //      1)ステージ上にマウスポインタがある かつ
63    //      2)全てのブロックが落下して消去ブロックが無い場合
64    if(pos.y >= game.offsetY && game.state == WAITING) game.state = SET_BLOCK;
65};
66 
67/*
68 * 背景を描画
69 */
70const drawBackground = ()=> {
71    let x = 0, y = 0;   // ステージ上のブロック座標
72 
73    // 市松模様のステージ色
74    let color1 = BACKGROUND_COLOR1;
75    let color2 = BACKGROUND_COLOR2;
76 
77    // 市松模様を描画
78    for(i=0; i<BLOCK_LENGTH; i++){
79        // 1段下がると先頭の色を入れ替える
80        if(i % COLUMN_SIZE == 0) [color1, color2] = [color2, color1];
81        // 格子状にブロックを表示
82        x = i % COLUMN_SIZE;            // x方向ブロック座標
83        y = Math.floor(i / ROW_SIZE);   // Y方向ブロック座標
84 
85        // 色を交互に設定
86        bgColor = (i % 2 == 0) ? color1: color2;
87        g.fillStyle = bgColor;
88        g.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE + game.offsetY, BLOCK_SIZE, BLOCK_SIZE);
89    }
90}
91 
92/*
93 * カーソルを描画
94 */
95const drawCursor = ()=> {
96    // ステージ外ではなにもしない
97    if(pos.y < game.offsetY) return;
98 
99    // マウス座標からブロック位置を求める
100    const x = Math.floor(pos.x / BLOCK_SIZE);
101    const y = Math.floor(pos.y / BLOCK_SIZE);
102 
103    g.fillStyle = "rgba(128, 128, 128, 0.5)";   // グレー色で透過
104    g.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
105}
106 
107/*
108 * ステージを表示
109 */
110const drawStage = ()=> {
111    // キャンバスクリア
112    g.clearRect(0, 0, canvas.width, canvas.height);
113    // 背景描画
114    drawBackground();
115    // カーソル描画
116    drawCursor();
117    // 次のブロックを表示
118    showNextBlock();
119 
120    // ステージ配列に基づき、ブロック画像を表示
121    for(let i=0; i<BLOCK_LENGTH; i++){
122        x = i % COLUMN_SIZE;            // x方向ブロック座標
123        y = Math.floor(i / ROW_SIZE);   // Y方向ブロック座標
124 
125        // ブロックを表示
126        g.drawImage(images[stageState[i]], x*BLOCK_SIZE, y*BLOCK_SIZE + game.offsetY);
127 
128        // ゲーム状態を表す配列内容を表示(デバッグ用)
129        if(isDebug){
130            g.fillStyle = "#fffa";
131            g.fillText(stageState[i], x*BLOCK_SIZE+20, y*BLOCK_SIZE+20+game.offsetY);
132        }
133    }
134}
135 
136/*
137 * 画像ファイル読み込み
138 */
139const loadImages = async ()=> {
140    const images = [];
141    const promises = [];
142 
143    image_files.forEach((url) =>{
144        const promise = new Promise((resolve)=>{
145            const img = new Image();
146            img.addEventListener("load", ()=>{
147                console.log("loaded " + url);
148                resolve();  // 読み込み完了通知
149            });
150            img.src = "./asset/" + url; // 画像ファイル読み込み開始
151            images.push(img);   // 画像オブジェクト配列に格納
152        });
153        promises.push(promise);
154    });
155    await Promise.all(promises);    // 全ての画像ファイル読み込みが完了してから
156    return images;                  // 画像オブジェクトを返す
157}
158 
159/*
160 * ステージ情報を生成
161 */
162const createStage = (stage)=> {
163    stageState = [];
164    stageState = [...stage];    // ステージ情報をコピー(stageStage <-- stage)
165    console.log(stageState);
166}
167 
168/*
169 * ブロックを落とす
170 */
171const dropBlock = ()=> {
172    // 落下タイミングチェック
173    timer.end = new Date().getTime();
174    const passedTime = (timer.end - timer.start) / 100; // 10分の1秒に直す
175 
176    if(passedTime <= dropSpeed){
177        return; // 落下しない
178    }
179 
180    let dropCount = 0;  // 落としたブロックの数
181    for(let y=ROW_SIZE-2; y>=0; y--){    // ステージの下方向からチェック
182        for(let x=0; x<COLUMN_SIZE; x++){
183            const upIndex = y*ROW_SIZE+x;
184            const downIndex = (y+1)*ROW_SIZE+x;
185            if(stageState[upIndex] != 0 && stageState[downIndex] == 0){
186                // 入れ替え
187                [ stageState[upIndex], stageState[downIndex] ]= [ stageState[downIndex], stageState[upIndex] ];
188                dropCount++;    // 落とすブロックの数をカウント
189            }
190        }
191    }
192    timer.start = new Date().getTime(); // 落下タイマー再スタート
193 
194    return dropCount;   // 落としたブロック数を返す
195}
196 
197/*
198 * ブロックが消去できるかチェックする
199 */
200const checkBlock = () =>{
201    // ステージの状態をコピー
202    const check = [...stageState];
203    // 消すブロックの数
204    let deleteBlockCount = 0;
205    // 横方向チェック
206    for(let y=0; y<ROW_SIZE; y++){
207        for(let x=1; x<COLUMN_SIZE-1; x++){
208            const currentIndex = y*ROW_SIZE + x;    // 中央
209            const rightIndex = y*ROW_SIZE + (x + 1);// 右側
210            const leftIndex = y*ROW_SIZE + (x - 1); // 左側
211            if(stageState[currentIndex] > 0){
212                if((stageState[currentIndex] === stageState[rightIndex] && stageState[currentIndex] === stageState[leftIndex])){
213                    check[currentIndex] = check[rightIndex] = check[leftIndex] = 7; // 7はこれから消す予定の×マーク画像
214                    deleteBlockCount++;
215                }
216            }
217        }
218    }
219    // 縦方向チェック
220    for(let y=1; y<ROW_SIZE-1; y++){
221        for(let x=0; x<COLUMN_SIZE; x++){
222            const currentIndex = y*ROW_SIZE + x;    // 中央
223            const upIndex = (y-1)*ROW_SIZE + x; // 上側
224            const downIndex = (y+1)*ROW_SIZE + x;   // 下側
225            if(stageState[currentIndex] > 0){
226                if((stageState[currentIndex] === stageState[upIndex] && stageState[currentIndex] === stageState[downIndex])){
227                    check[currentIndex] = check[upIndex] = check[downIndex] = 7;    // 7はこれから消す予定の×マーク画像
228                    deleteBlockCount++;
229                }
230            }
231        }
232    }
233 
234    // チェック後の状態をステージ情報に戻す
235    stageState = [...check];
236    return deleteBlockCount;
237}
238 
239/*
240 * ブロックを消去する
241 */
242const deleteBlock = () =>{
243    for(let i=0; i<BLOCK_LENGTH; i++){
244        if(stageState[i] == 7){ // ×画像だったら
245            stageState[i] = 0;  // 何もない画像にする
246        }
247    }
248}
249 
250/*
251 * 最上段にランダムにブロックを配置する
252 */
253const setRandomBlock = () =>{
254    for(let i=0; i<COLUMN_SIZE; i++){
255        const blockType = Math.floor(Math.random() * (images.length-1));    // 0~6
256        stageState[i] = blockType;
257    }
258}
259 
260/*
261 * 次のブロックをランダムで決定する
262 */
263const setNextBlock = () =>{
264    // 次のブロックタイプをランダムで設定
265    nextBlockType = Math.floor(Math.random() * (images.length-2)) + 1;  // 1~6
266    console.log("NEXT: " + image_files[nextBlockType]);
267}
268 
269/*
270 * 次のブロックを表示する
271 */
272const showNextBlock = () =>{
273    // 文字と背景
274    g.fillStyle = "#555";
275    g.fillText("NEXT", 324, 50);
276 
277    g.fillStyle = "#ddd";
278    g.fillRect(384, 0, 128, 128);
279 
280    // 次のブロックを表示
281    g.drawImage(images[nextBlockType], 424, 35, 50, 50);
282}
283 
284/*
285 * ゲーム状態を表示(デバッグ表示用)
286 */
287const showGameState = () =>{
288    // Symbolのdescriptionを表示する
289    g.fillStyle = "yellow";
290    g.fillText(game.state.description, 220, 220 + game.offsetY);
291}
292 
293/*
294 * ゲームのメイン処理(少しだけゲームっぽくなってきている)
295 */
296const GameMain = () =>{
297    // ステージ表示
298    drawStage();
299 
300    // ゲーム状態を表示(デバッグ用)
301    if(isDebug){
302        showGameState();
303    }
304 
305    // ゲーム状態(game.state)による分岐処理
306    if(game.state == DROP){
307        // ブロックを落とす
308        if(dropBlock() == 0){   // 落とすブロックが無くなったら
309            game.state = CHECK_BLOCK;
310        }
311    }
312    else if(game.state == CHECK_BLOCK){
313        // 消せるブロックをチェックする
314        if(checkBlock() != 0){
315            game.state = DELETE_BLOCK;
316        }
317        else{
318            game.state = WAITING;   // 消すブロックがないなら入力待ち
319        }
320    }
321    else if(game.state == DELETE_BLOCK){
322        // ブロックを消去
323        setTimeout( function(){
324            deleteBlock();
325            game.state = DROP;
326        }, 200);    // すぐに消去せず、どれが消去ブロックが分かるように200ミリ秒の間×画像を表示している
327    }
328    else if(game.state == SET_BLOCK){
329        // ステージ最上段にランダムにブロックを配置
330        setRandomBlock();
331        // ブロックタイプを更新
332        currentBlockType = nextBlockType;
333        // 次のブロックタイプを設定
334        setNextBlock();
335        // ステージのクリック位置に画像ブロックを設定
336        const blockPos = {
337            x: Math.floor(pos.x / BLOCK_SIZE),                  //ブロックx座標
338            y: Math.floor((pos.y-game.offsetY) / BLOCK_SIZE)    // ブロックのy座標
339        };
340 
341        stageState[blockPos.x + blockPos.y * COLUMN_SIZE] = currentBlockType;
342        console.log(blockPos);
343 
344        // ブロックを落とすモードに移行
345        game.state = DROP;
346    }
347    else if(game.state == WAITING){
348        // ブロック配置待ち(現時点では何もしない)
349    }
350    // フレーム再描画
351    requestAnimationFrame(GameMain);
352}
353 
354/*
355 * 起動時の処理
356 */
357window.addEventListener("load", async ()=>{
358    // キャンバス取得
359    canvas = document.getElementById("canvas");
360    g = canvas.getContext("2d");
361    g.font = "normal 30px Impact"// フォントサイズ・フォント種類 設定
362    g.textBaseline = "top";         // テキスト描画時のベースライン
363 
364    // マウスイベント設定
365    canvas.addEventListener("mousemove", mouseMoveListener, false);
366    canvas.addEventListener("mousedown", mouseDownListener, false);
367 
368    // 画像ファイルを読み込む
369    images = await loadImages(image_files);
370    console.log(images);
371 
372    // ステージ情報を生成する
373    const stage1 = [
374        0, 0, 0, 0, 0, 0, 0, 0,
375        0, 0, 0, 0, 0, 0, 0, 0,
376        0, 0, 0, 0, 0, 0, 0, 0,
377        0, 0, 0, 0, 0, 0, 0, 0,
378        0, 0, 0, 0, 0, 0, 0, 0,
379        0, 0, 0, 0, 0, 0, 0, 0,
380        0, 0, 0, 0, 0, 0, 0, 0,
381        0, 0, 0, 0, 0, 0, 0, 0
382    ];
383    createStage(stage1);
384 
385    // 落下タイマースタート
386    timer.start = new Date().getTime();
387 
388    // 最上段にランダムにブロックを配置
389    setRandomBlock();
390 
391    // 最初のブロックを決定
392    setNextBlock();
393 
394    // 落下モード
395    game.state = DROP;
396 
397    // ゲーム開始
398    GameMain();
399});
関連記事

コメント

タイトルとURLをコピーしました