セイムゲームのソースコード
「セイムゲーム作ったよ」で紹介した奴のソースコードです。最初にソースコードを添付しておきます(後に掲載しているものと同じです)。
samegame.zip
方針
以前オセロを作ったのですが、オブジェクト指向的に書くということを目的にしていたので、少し大げさな仕様になってしまいました。今回の方針として、逆にあっさりとゲームを書こうとしたらどんな実装になるのかと思い作ってみました。
処理の流れ解説
最初にinit()という関数が呼ばれ、必要なDOM要素が作成されます。次にpaint()が呼ばれ、実際に画面にブロックが表示されます。あとは、ブロックをクリックした際に、onclickのイベントで、selectEvent()を呼びます。selectEventが実行されると、その内部で次はブロックを削除するためにcheck()を呼びます。ブロックを削除した後はブロックを落としてくる必要があるので、downBlockSequence()を呼びます。downBlockSequenceの中で、下側にブロックを落とす処理moveBottom()と、左側にブロックをつめる処理moveLeft()を呼びます。downBlockSequenceが終わると、最後にゲームの終了判定をcheckEndGame()というメソッドで行います。
このゲームには消しゴムというアイテムがあります。消しゴムをクリックした場合、ゲームの状態を保持するstatusという変数が"kesigom"という値になります。通常この値は"normal"という値を示しているのですが、"kesigom"という値のときに、ブロックがクリックされると、普段とは違う処理が行われます。またゲーム終了状態を示す"end"という値も取ります。この状態の場合、ブロックがクリックされても何もしません。
アルゴリズム
少し複雑なアルゴリズムをcheck()とcheckRecursive()に取り入れています。これは繋がっているブロックを探して消去するものです。このアルゴリズムについては、別の記事で解説しています。
次に、moveBottom()とmoveLeft()にも、check()ほどではありませんが、少し分かりにくいアルゴリズムを導入しています。これは配列の隙間をつめるためのアルゴリズムで、アルゴリズム教室を参考にさせていただきました。
ソースコード
短いものから載せていきます。まずindex.html
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>セイムゲーム</title> <script src="animation.js"></script> <script src="samegame.js"></script> </head> <body onload="init()" style="padding:20px"> <div id ="samegame"></div> </body> </html>
次にちょっとしたライブラリ。animation.js。DOM要素を作成します。Animationという名前がついてるけど、特にアニメーションをするわけではないです。
var Animation = { /** * キャンバスを作成する * @param {Object} x * @param {Object} y * @param {Object} width */ createCanvas : function(w,h,id,style){ var element = document.createElement("div"); if(id != null) element.id = id; if(w != null) element.style.width = w + "px"; if(h != null) element.style.height = h + "px"; element.style.overflow = "hidden"; element.style.position = "relative"; if(style != null) element.style.cssText += ";"+style; return element; }, createCanvasObject : function(w,h,id,img,style){ var element = document.createElement("div"); if(id != null) element.id = id; if(w != null) element.style.width = w + "px"; if(h != null) element.style.height = h + "px"; element.style.overflow = "hidden"; element.style.position = "absolute"; if(style != null) element.style.cssText += ";"+style; if (img != null) { var c_img = document.createElement("img"); c_img.src = img; element.appendChild(c_img); } return element; } };
最後に本体。正直あまり綺麗な処理ではないです。
/** * セイムゲーム * http://d.hatena.ne.jp/prime503/ */ var canvas; //ゲームを描写するDOM要素 var least_element; //消しゴムの残り回数を表示するDOM要素 var score_element; //スコアを表示するDOM要素 var cells; //セルを表す二次元配列 var score; //得点 var status; //ゲームの状態 var kesigom_least; //消しゴムが使える回数 var CELL_X = 15; var CELL_Y = 10; var CELL_W = 60; Type = function(mark,color){ this.mark = mark; this.color = color; Type.prototype.validate = function(){ for (var p in Type) { if (Type[p] == this){ return true; } } return false; }; }; Type = { BLOCK1 : new Type("■","blue"), BLOCK2 : new Type("●","skyblue"), BLOCK3 : new Type("▲","green"), BLOCK4 : new Type("★","orange"), BLOCK5 : new Type("◆","gray"), BLOCK6 : new Type("▼","red"), EMPTY : new Type() }; function init(){ score = 0; status = "normal"; kesigom_least = 3; var samegameElement = document.getElementById("samegame"); samegameElement.innerHTML =""; samegameElement.style.position="relative"; //15*10のセルを作る cells = new Array(CELL_X); for (var i = 0; i < cells.length; i++) { cells[i] = new Array(CELL_Y); for (var j = 0; j < cells[i].length; j++) { var random = Math.floor(Math.random() * 6 + 1); switch (random) { case 1: cells[i][j] = Type.BLOCK1; break; case 2: cells[i][j] = Type.BLOCK2; break; case 3: cells[i][j] = Type.BLOCK3; break; case 4: cells[i][j] = Type.BLOCK4; break; case 5: cells[i][j] = Type.BLOCK5; break; case 6: cells[i][j] = Type.BLOCK6; break; } } } canvas = Animation.createCanvas(CELL_X * CELL_W, CELL_Y * CELL_W, "canvas","background-color:#ddd;"); samegameElement.appendChild(canvas); //消しゴム var kesigom = Animation.createCanvasObject(null,null,"kesigom","kesi.png","left:20px;top:20px;position:relative;overflow:visible;float:left;"); kesigom.onclick=function(){ itemEvent(ItemType.KESIGOM); }; //消しゴムの残り回数を表示 least_element = Animation.createCanvasObject(58,58,"least",null,"font-size:30px;font-weight:bold;color:red;overflow:visible;position:relative;"); //数字の部分 kesigom.appendChild(least_element); samegameElement.appendChild(kesigom); //得点を表示する部分 score_element = Animation.createCanvasObject(300,58,"score",null,"background-color:#ddd;font-size:50px;font-weight:bold;color:blue;text-align:center;position:relative;top:20px;left:200px;"); samegameElement.appendChild(score_element); paint(); }; function paint(){ canvas.innerHTML =""; for (var i = 0; i < cells.length; i++) { for (var j = 0; j < cells[i].length; j++) { var color = cells[i][j].color; var mark = cells[i][j].mark; if(color == null) continue; var block = Animation.createCanvasObject(CELL_W,CELL_W,"cell",null, "color:"+color+";font-size:"+(CELL_W-10)+"px;left:"+ (i * CELL_W) + "px;top:"+ j*CELL_W + "px;text-align:center"); block.innerHTML =mark; (function(){ var _i = i, _j = j; block.onclick = function(){ selectEvent(_i, _j); }; })(); canvas.appendChild(block); } } least_element.innerHTML="× " + kesigom_least; score_element.innerHTML= score; } /** * ブロックが選択されたときの処理 * @param {Object} x * @param {Object} y */ function selectEvent(x,y){ if(status == "end") return; else if (status == "normal") { /** * check関数で繋がっているブロックを探索 */ //2つ以上繋がってるならブロックを消す var count = check(2, x, y); if (!(count >= 2)) //ブロックを消せないならここで終わり return; //ブロックをつめる downBlockSequence(); //終了判定 checkEndGame(); }else if(status == "kesigom"){ if (cells[x] == null || cells[x][y] == null || cells[x][y] == Type.EMPTY) { }else{ cells[x][y] = Type.EMPTY; //ブロックをつめる downBlockSequence(); } status = "normal"; //通常モードに戻す } } /** * 近接する同じ種類のブロックを探す * checkTypeの値を1に指定すると、近接するブロックの数を返す * checkTypeの値を2に指定すると、近接するブロックが2個以上の場合消去し、消去できた数を返す. * @param {Object}checkType チェックの種類 * @param {Object} x ブロックを置く位置 * @param {Object} y */ function check(checkType, x, y){ if(cells[x] == null || cells[x][y] == null || cells[x][y] == Type.EMPTY ) return 0; //範囲外か空のブロック /* * 探索用に二次元配列を作る. * 未探索は-1 * 探索済み (x,y)にあるブロックと同じ種類のブロックは1 * 違う種類のブロック,空のブロックは 2 */ var blockType = cells[x][y]; var cells_check = new Array(CELL_X); for (var i = 0; i < cells_check.length; i++) { cells_check[i] = new Array(CELL_Y); for (var j = 0; j < cells_check[i].length; j++) { cells_check[i][j] = -1; //未チェック } } checkRecursive(x, y, cells_check, blockType); //隣接するブロックの数を数える var count = 0; for (var i = 0; i < cells_check.length; i++) { for (var j = 0; j < cells_check[i].length; j++) { if (cells_check[i][j] == 1) { count++; } } } //ブロック消去 if (checkType == 2) { if (count >= 2) { score += count * 10; for (var i = 0; i < cells_check.length; i++) { for (var j = 0; j < cells_check[i].length; j++) { if (cells_check[i][j] == 1) { cells[i][j] = Type.EMPTY; } } } }else{ //消去失敗 return 0; } } return count; } /** * @param {Object} x * @param {Object} y * @param {Object} cells_check 二次元配列 * @param {Object} blockType ブロックの種類 */ function checkRecursive(x, y, cells_check,blockType){ if(cells[x] == null || cells[x][y] == null || 0 < cells_check[x][y]) return; //範囲外か探索済み if (cells[x][y] != blockType) { //ブロックの種類が違う cells_check[x][y] = 2; return; } //一致 cells_check[x][y] = 1; //チェック checkRecursive( x + 1, y, cells_check, blockType); checkRecursive( x - 1, y, cells_check, blockType); checkRecursive( x, y + 1, cells_check, blockType); checkRecursive( x, y - 1, cells_check, blockType); return; } /** * ブロックを詰めていく */ function downBlockSequence(){ moveBottom(); //ブロックを下側に詰める paint(); moveLeft(); //ブロックを左側に詰める setTimeout(paint, 250); //時間差を入れて描写 } /** * ブロックの下に隙間があれば詰める * * ■ * ■ * × * ■ * * ↓ * * ■ * ■ * ■ * */ function moveBottom(){ for (var i = 0; i < cells.length; i++) { /* * 配列の隙間を詰めるアルゴリズム * 参考 http://www.geocities.jp/oldbig_ancient/KodaiHP3.htm */ var pointer=cells[0].length - 1; //配列要素の移動先 for (var j = cells[0].length - 1; j >= 0; j--) { var currentCell = cells[i][j]; if(j != pointer && currentCell != Type.EMPTY){ cells[i][pointer] = currentCell; cells[i][j] = Type.EMPTY; } if(currentCell.validate() && currentCell != Type.EMPTY){ //規定されているブロックなおかつ、空でない pointer--; } } } } /** * ブロックの左に列単位の隙間があれば列ごと左につめる * * ■ * ■ ■ * ■ ■ * * ↓ * * ■ * ■■ * ■■ * */ function moveLeft(){ /* * 配列の隙間を詰めるアルゴリズム * 参考 http://www.geocities.jp/oldbig_ancient/KodaiHP3.htm */ var pointer=0; //配列要素の移動先 for (var i = 0; i < cells.length; i++) { var currentCell = cells[i][cells[0].length - 1]; //一番下のブロック if (i != pointer && currentCell != Type.EMPTY) { //縦列ごと移動 for (var k = 0; k < cells[0].length; k++) { cells[pointer][k] = cells[i][k]; cells[i][k] = Type.EMPTY; } } if (currentCell.validate() && currentCell != Type.EMPTY) { //規定されているブロックなおかつ、空でない pointer++; } } } function checkEndGame(){ //ゲームの終了判定. var isEnd = true; loop: for (var i = 0; i < cells.length; i++) { for (var j = 0; j < cells[0].length; j++) { if (check(1, i, j) >= 2) { isEnd = false; break loop; } } } if (isEnd) { setTimeout(function(){ //setTimeoutを使って、returnの後で実行 status ="end"; var last = 0; //残りブロックの数 for (var i = 0; i < cells.length; i++) { for (var j = 0; j < cells[0].length; j++) { if (cells[i][j].validate() && cells[i][j] != Type.EMPTY) last++; } } if(last <= 10){ //残りブロックが10個以内ならボーナス var compl = 10 - last; var bonus = (compl + 50) * ( compl * 4 + 10); }else{ bonus = 0; } var kesi = kesigom_least * 300; var total = score + bonus + kesi; var str ="ゲーム終了"; str+= "\nブロック消去:" +score; str += "\n残りブロック ×"+last+" ボーナス:"+bonus; str += "\n残りアイテム ×"+kesigom_least+" ボーナス:"+kesi; str += "\n合計点:"+total; alert(str); score = total; paint(); var endmessage = Animation.createCanvasObject(CELL_W*CELL_X,null,null,null, "color:black;font-size:200px;text-align:center;top:200px"); endmessage.innerHTML="END"; canvas.appendChild(endmessage); }, 250); return; } } ItemType=function(){ ItemType.prototype.validate = function(){ for (var p in ItemType) { if (ItemType[p] == this){ return true; } } return false; }; }; ItemType={KESIGOM:new ItemType()}; function itemEvent(type){ if(!type || !type.validate()) throw new Erorr("itemEvent内でエラー"); if(status == "end") return; //無視 if(type == ItemType.KESIGOM && kesigom_least > 0){ status ="kesigom"; kesigom_least--; paint(); } }