[お題にTRY] Canvas三目並べ #7 : 仕上げのリファクタリング

2020年6月29日

Javascript テクノロジー プログラミング 特集

自宅のwifiが比較的弱い、ユゲタです。 ビデオ閲覧が主になってしまったネット環境では、弱いwifiを使い続けることはできませんが、いいwifiルータに出会うことができません。 これまで10個ぐらいwifi機器を買い替えてきたんですが、未だにアタリに出会った気がしません。 一体アタリの機器って、何を買えば良いんだろう?

本日のIT謎掛け

「wifi」と、かけまして・・・ 「無謀な自動車運転」と、ときます。 そのココロは・・・ どちらも、信号を、とばします。

本日の内容

いよいよ、千秋楽となった三目並べですが、本日は、リプレイ機能を付けて、1ページロードで1回しかできないゲームを、 無限で遊べるようにしてみます。 あと、最後なので、気になる箇所を、リファクタリングをしてみたいと思います。

ソースコード

(function(){ var LIB = function(){}; LIB.prototype.event = function(target, mode, func , flg){ flg = (flg) ? flg : false; if (target.addEventListener){target.addEventListener(mode, func, flg)} else{target.attachEvent('on' + mode, function(){func.call(target , window.event)})} }; LIB.prototype.construct = function(){ switch(document.readyState){ case "complete" : new MAIN();break; case "interactive" : this.event(window , "DOMContentLoaded" , (function(){new MAIN()}).bind(this));break; default : this.event(window , "load" , (function(){new MAIN()}).bind(this));break; } }; var MAIN = function(){ this.mode = ""; // 棋譜格納用のバッファ this.buffer = []; // 先手、後手のターンフラグ this.turn = null; this.drawBase(); this.setEvent(); this.startDecide(); }; MAIN.prototype.drawBase = function(){ var ctx = document.getElementById("mycanvas").getContext("2d"); // 背景 ctx.fillStyle = "#14bdac"; ctx.fillRect(0,0,400,200); // 枠線 ctx.strokeStyle = "#0da192"; ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(170,20); ctx.lineTo(170,180); ctx.stroke(); ctx.fill(); ctx.beginPath(); ctx.moveTo(230,20); ctx.lineTo(230,180); ctx.stroke(); ctx.fill(); ctx.beginPath(); ctx.moveTo(120,70); ctx.lineTo(280,70); ctx.stroke(); ctx.fill(); ctx.beginPath(); ctx.moveTo(120,130); ctx.lineTo(280,130); ctx.stroke(); ctx.fill(); }; MAIN.prototype.setEvent = function(){ var ctx = document.getElementById("mycanvas"); new LIB().event(ctx , "click" , (function(e){this.clickCtx(e)}).bind(this)); new LIB().event(ctx , "mousemove" , (function(e){this.mouseMoveCtx(e)}).bind(this)); }; // 先手、後手を決めるダイアログ表示 MAIN.prototype.startDecide = function(mode){ this.mode_dialog = typeof mode === "undefined" ? null : mode; this.makeDialog( { color_bg : "rgb(250, 148, 47)", color_stroke : "rgb(255, 127, 0)" }, ["選択してください。"] ); // 先攻 var col1 = this.mode_dialog === 0 ? "red" : "gray"; this.makeDialog( { color_bg : col1, color_txt : "white", alitn : "left", margin : 0, padding : "5px", width : "80px", height : "40px", textSize : "20px", x : "100px", y : "100px" }, ["先攻"] ); // 後攻 var col2 = this.mode_dialog === 1 ? "red" : "gray"; this.makeDialog( { color_bg : col2, color_txt : "white", alitn : "left", margin : 0, padding : "5px", width : "80px", height : "40px", textSize : "20px", x : "220px", y : "100px" }, ["後攻"] ); this.mode = "dialog_start"; }; var __dialogOptions = { color_bg : "red", color_txt : "", color_stroke : "transparent", strokeWidth : 0, align : "center", // [ left , center , right ] valign : "middle", // [ top , middle , bottom ] width : "70%", // % or px *数値のみの場合はpx height : "60%", // % or px *数値のみの場合はpx margin : "10px", // 全ての方向に対して一律指定 : %指定の場合はwidthに対しての% or px *数値のみの場合はpx padding : "10px", // 全ての方向に対して一律指定 : %指定の場合はwidthに対しての% or px *数値のみの場合はpx radius : "8px", textAlign : "center", textFont : "sans-serif", textColor : "white", textBase : "top", textWeight: "bold", textSize : "16px", x : 0, // 自動設定 y : 0, // 自動設定 tx : 0, // 自動設定 ty : 0, // 自動設定 $:0 } // ダイアログの表示 // options : __dialogOptions is default // texts : array strings of text-line MAIN.prototype.makeDialog = function(options , texts){ options = this.setOptions(__dialogOptions , options); options = this.dialogOptions_adjust(options); var canvas = document.getElementById("mycanvas"); var ctx = canvas.getContext("2d"); // dialog描画 ctx.fillStyle = options.color_bg; ctx.strokeStyle = options.color_stroke; ctx.lineWidth = options.strokeWidth; if(options.radius === 0){ ctx.fillRect(options.x , options.y , options.width , options.height); } else{ var w = options.width; var h = options.height; var x = options.x; var y = options.y; var r = options.radius; ctx.beginPath(); ctx.moveTo(x,y + r); ctx.arc(x+r , y+h-r , r , Math.PI , Math.PI*0.5 , true); ctx.arc(x+w-r , y+h-r , r , Math.PI*0.5,0 , 1); ctx.arc(x+w-r , y+r , r , 0 , Math.PI*1.5 , 1); ctx.arc(x+r , y+r , r , Math.PI*1.5 , Math.PI , 1); ctx.closePath(); ctx.stroke(); ctx.fill(); } // 文字 ctx.fillStyle = options.textColor; ctx.font = options.textWeight +" "+ options.textSize +"px "+ options.textFont; ctx.textAlign = options.textAlign; ctx.textBaseline = options.textBase; for(var i=0; i<texts.length; i++){ ctx.fillText(texts[i], options.tx, options.ty + (i * options.textSize + 4) , options.width); } } // @px,@%を数値に変換する MAIN.prototype.px_per2num = function(data , max){ if(String(data).match(/^([0-9]+?)%$/)){ var num = Number(RegExp.$1); num = num <= 100 ? num : 100; return parseInt(max * (num / 100)); } else if(String(data).match(/^([0-9]+?)px$/)){ return Number(String(data).replace("px","")); } else{ return Number(data); } }; MAIN.prototype.dialogOptions_adjust = function(options){ var canvas = document.getElementById("mycanvas"); // サイズ処理 options.width = this.px_per2num(options.width , canvas.offsetWidth); options.height = this.px_per2num(options.height , canvas.offsetHeight); options.margin = this.px_per2num(options.margin , canvas.offsetWidth); options.padding = this.px_per2num(options.padding , canvas.offsetWidth); options.textSize = Number(String(options.textSize).replace("px","")); options.radius = this.px_per2num(options.radius , canvas.offsetWidth); if(options.x === 0){ switch(options.align){ case "left": options.x = options.margin; break; case "right": options.x = canvas.offsetWidth - (options.width + options.margin); break; case "center": default: options.x = (canvas.offsetWidth / 2) - (options.width / 2); break; } } else{ options.x = this.px_per2num(options.x , canvas.offsetWidth); } if(options.y === 0){ switch(options.valign){ case "top": optinos.y = options.margin; break; case "bottom": options.y = canvas.offsetHeight - (options.height + options.margin); break; case "middle": default: options.y = (canvas.offsetHeight / 2) - (options.height / 2); break; } } else{ options.y = this.px_per2num(options.y , canvas.offsetHeight); } // text情報 options.ty = options.y + options.padding; switch(options.textAlign){ case "left": options.tx = options.x + options.padding; break; case "right": options.tx = canvas.offsetWidth - options.x - options.padding; break; case "center": default: options.tx = options.x + (options.width / 2); break; } return options; }; MAIN.prototype.dialog_start = function(e){ var canvas = e.currentTarget; var x = e.clientX - canvas.getBoundingClientRect().left; var y = e.clientY - canvas.getBoundingClientRect().top; // 先攻エリア if(this.check_pos[0](x,y) === true && this.mode_dialog !== 1){ this.clearCTX(); this.drawBase(); this.startDecide(0); } // 後攻エリア else if(this.check_pos[1](x,y) && this.mode_dialog !== 0){ this.clearCTX(); this.drawBase(); this.startDecide(1); } // 戻す else if(this.mode_dialog !== null){ this.clearCTX(); this.drawBase(); this.startDecide(null); } }; // デフォルトoption設定に指定するoptionを上書きする処理(データ1階層のみ対応) MAIN.prototype.setOptions = function(baseOption , newOptions){ newOptions = newOptions ? newOptions : {}; var res = JSON.parse(JSON.stringify(baseOption)); for (var i in newOptions) {res[i] = newOptions[i];} return res; }; // ユーザーの番(先手) MAIN.prototype.clickCtx = function(e){ switch(this.mode){ case "dialog_start": this.clickStartDecide(e); break; case "oncemore": this.clickOncemore(e); break; default: this.putMark(e); } }; // ボタンの座標判定 // [0] : 先攻ボタン // [1] : 後攻ボタン // [2] : もう一回ボタン MAIN.prototype.check_pos = [ function(x,y){ if(x >= 100 && x <= 180 && y >= 100 && y <= 140){ return true; } else{ return false; } }, function(x,y){ if(x >= 220 && x <= 300 && y >= 100 && y <= 140){ return true; } else{ return false; } }, function(x,y){ if(x >= 140 && x <= 260 && y >= 100 && y <= 140){ return true; } else{ return false; } } ]; MAIN.prototype.clickStartDecide = function(e){ var canvas = e.currentTarget; var x = e.clientX - canvas.getBoundingClientRect().left; var y = e.clientY - canvas.getBoundingClientRect().top; // 座標判定 if(this.check_pos[0](x,y) === false && this.check_pos[1](x,y) === false){return;} this.clearCTX(); this.drawBase(); this.mode = null; // 先攻 if(this.check_pos[0](x,y) === true){ this.turn = 0; } // 後攻 else{ this.turn = 1; this.turnComputer(); } }; MAIN.prototype.clickOncemore = function(e){ var canvas = e.currentTarget; var x = e.clientX - canvas.getBoundingClientRect().left; var y = e.clientY - canvas.getBoundingClientRect().top; // 座標判定 if(this.check_pos[2](x,y) === false){return;} // フラグリセット this.buffer = []; this.finish_mode = null; // スタート画面 this.clearCTX(); this.drawBase(); this.startDecide(null); }; MAIN.prototype.putMark = function(e){ if(this.turn !== 0){return;} var canvas = e.currentTarget; // canvas内のクリックされた座標 var posX = e.clientX - canvas.getBoundingClientRect().left; var posY = e.clientY - canvas.getBoundingClientRect().top; // 座標から枠判定 [1-9] var cell = this.checkCell(posX , posY); if(!cell){return;} // バッファに登録(登録済みのセルの場合は、処理しない) if(this.containNum(cell)){return;} this.buffer.push({ mark : 0, num : cell }); // 記号を表示する this.drawBuffer(); // 判定 if(this.judgement()){return;} this.turn = 1; // 打つ手が無くなったら終了 if(this.buffer.length >= 9){ this.draw(); this.finish(); return; } // 続けてコンピュータの手 var time = this.getRandomTime(1000); setTimeout((function(){this.turnComputer();}).bind(this) , time); } MAIN.prototype.mouseMoveCtx = function(e){ switch(this.mode){ case "dialog_start": this.dialog_start(e); break; case "oncemore": this.mouseover_oncemore(e); break; } }; MAIN.prototype.getRandomTime = function(max_time){ return Math.floor(Math.random() * max_time); }; MAIN.prototype.cell = { x : [118,177,236], y : [18,76,134], size : 47 } MAIN.prototype.checkCell = function(posX , posY){ var cnt = 1; for(var i=0; i<this.cell.y.length; i++){ for(var j=0; j<this.cell.x.length; j++){ if(this.cell.x[j] <= posX && posX <= this.cell.x[j] + this.cell.size && this.cell.y[i] <= posY && posY <= this.cell.y[i] + this.cell.size){ return cnt; } cnt++; } } return null; }; MAIN.prototype.clearCTX = function(){ var canvas = document.getElementById("mycanvas"); var ctx = canvas.getContext("2d"); ctx.clearRect(0, 0, canvas.width, canvas.height); }; MAIN.prototype.drawBuffer = function(){ // 描画をクリアする this.clearCTX(); // 基盤を表示 this.drawBase(); // バッファ全ての座標を指定 for(var i=0; i<this.buffer.length; i++){ var num = this.buffer[i].num; // 奇数番 if(i % 2 === 0){ this.drawMark_0(num); } // 偶数 else{ this.drawMark_1(num); } } }; // player-mark @ [0:○ , 1:×] MAIN.prototype.drawMark_0 = function(cell){ var ctx = document.getElementById("mycanvas").getContext("2d"); var x = y = null , r=16; var offset = 24; var x = this.cell.x[(function(cell){var a = cell % 3 - 1;return a < 0 ? 2 : a})(cell)] + offset; var y = this.cell.y[parseInt((cell-1) / 3 , 10)] + offset; if(x === null && y === null){return;} ctx.strokeStyle = "white"; ctx.beginPath(); ctx.arc(x,y,r, 0/180*Math.PI , 360/180*Math.PI); ctx.stroke(); }; // com-mark @ [0:○ , 1:×] MAIN.prototype.drawMark_1 = function(cell){ var ctx = document.getElementById("mycanvas").getContext("2d"); var size1 = 8, size2 = 38; var x = this.cell.x[(function(cell){var a = cell % 3 - 1;return a < 0 ? 2 : a})(cell)]; var y = this.cell.y[parseInt((cell-1) / 3 , 10)]; var x1 = x + size1; var y1 = y + size1; var x2 = x + size2; var y2 = y + size2; if(x1 === null && y1 === null && x2 === null && y2 === null){return;} ctx.strokeStyle = "white"; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke(); ctx.fill(); ctx.strokeStyle = "white"; ctx.beginPath(); ctx.moveTo(x1,y2); ctx.lineTo(x2,y1); ctx.stroke(); ctx.fill(); }; MAIN.prototype.turnComputer = function(){ if(this.turn !== 1){return;} // 打つ手が無くなったら終了 if(this.buffer.length >= 9){return;} // 現在の盤から、空きセルを検索 var emptyCells = this.getEmptyCells(); var cellNum = null; // 場所を決める(2手目判定) cellNum = this.argo_2(); // 自分リーチの時に終了させる if(!cellNum){ cellNum = this.getComReach(emptyCells); } // 相手がリーチの時に阻止する if(!cellNum){ cellNum = this.getUserReach(emptyCells); } // それ以外の場所 if(!cellNum){ cellNum = this.getComPlace(emptyCells); } this.buffer.push({ mark : 1, num : cellNum }); // 記号を表示する this.drawBuffer(); this.turn = 0; // 判定 if(this.judgement()){return;} // 打つ手が無くなったら終了 if(this.buffer.length >= 9){ this.draw(); this.finish(); } }; MAIN.prototype.getEmptyCells = function(){ var arr = []; for(var i=1; i<=9; i++){ if(this.containNum(i)){continue;} arr.push(i); } return arr; } MAIN.prototype.containNum = function(num){ for(var i in this.buffer){ if(this.buffer[i].num === num){ return this.buffer[i]; } } }; MAIN.prototype.containNum_player = function(mark , num){ for(var i in this.buffer){ if(this.buffer[i].mark === mark && this.buffer[i].num === num){ return this.buffer[i]; } } }; MAIN.prototype.getComPlace = function(emptyCells){ var num = Math.floor(Math.random() * emptyCells.length); return emptyCells[num]; } MAIN.prototype.finish = function(){ console.log("Finish !"); }; // mark @ 対象のコマ(マーク)[0:○ , 1:×] : int // return @ 3つ並んだら並んでいるマーク(0:○ , 1:×):int を返す , それ以外はnull MAIN.prototype.judgement = function(){ if(this.buffer.length < 3){return 0;} // プレイヤーの勝ち if(this.judgement_player(0)){ return this.win(0); } // コンピュータの勝ち else if(this.judgement_player(1)){ return this.win(1); } return 0; } MAIN.prototype.judgement_player = function(mark){ // 横一列判定 for(var i in win_pattern){ // [0-2] var cnt = 0; for(var j in win_pattern[i]){ var res = this.containNum(win_pattern[i][j]) // bufferに含まれていなければ、NG if(!res){break;} // 違うプレイヤーであれば、NG if(res.mark !== mark){break;} cnt += 1; } if(cnt >= 3){ return true; } } return false; } // 勝ち配列 var win_pattern = [ [1,2,3], [4,5,6], [7,8,9], [1,4,7], [2,5,8], [3,6,9], [1,5,9], [3,5,7] ]; MAIN.prototype.win = function(mark,mouseover){ if(mark !== 0 && mark !== 1){return null;} var user = mark === 0 ? "あなた" : "コンピュータ"; var text = user + "の勝ち"; this.finish_mode = mark; this.makeDialog( { color_bg : "orange", color_txt : "white", alitn : "left", margin : 0, padding : "5px", width : "70%", height : "100px", textSize : "20px" }, [text] ); this.oncemore(mouseover); return 1; }; MAIN.prototype.draw = function(mouseover){ this.finish_mode = 2; this.makeDialog( { color_bg : "orange", color_txt : "white", alitn : "left", margin : 0, padding : "5px", width : "70%", height : "100px", textSize : "20px" }, ["引き分け"] ); this.oncemore(mouseover); return 1; } MAIN.prototype.oncemore = function(mouseover){ var color = !mouseover ? "gray" : "red"; this.makeDialog( { color_bg : color, color_txt : "white", alitn : "left", margin : 0, padding : "5px", width : "120px", height : "40px", textSize : "20px", y : "100px" }, ["もう1回やる"] ); this.mode = "oncemore"; return 1; } MAIN.prototype.mouseover_oncemore = function(e){ var canvas = e.currentTarget; var x = e.clientX - canvas.getBoundingClientRect().left; var y = e.clientY - canvas.getBoundingClientRect().top; if(this.check_pos[2](x,y) === true){ if(this.finish_mode === 0){ this.win(0 , "on"); } else if(this.finish_mode === 1){ this.win(1 , "on"); } else if(this.finish_mode === 2){ this.draw("on"); } } else{ if(this.finish_mode === 0){ this.win(0); } else if(this.finish_mode === 1){ this.win(1); } else if(this.finish_mode === 2){ this.draw(); } } }; // コンピュータが2手目の判定 MAIN.prototype.argo_2 = function(){ if(this.buffer.length != 1){return;} for(var i=0; i<__argo2_data.length; i++){ for(var j=0; j<__argo2_data[i][0].length; j++){ if(this.buffer[0].num === __argo2_data[i][0][j]){ var lists = __argo2_data[i][1]; var lists = this.excludeLists(lists , [this.buffer[0].num]); return this.getComPlace(lists); } } } }; // 2手目の状態の時に1手目が置いたコマによって次に置くマス(置いてはいけないマス以外) // ※1手目と重複するマスは除外する var __argo2_data = [ [[1,3,7,9] , [1,2,3,4,6,7,8,9]], [[5] , [2,4,6,8]], [[2] , [4,6,7,9]], [[4] , [2,3,8,9]], [[6] , [1,2,7,8]], [[8] , [1,3,4,6]] ]; // リスト内に指定データ(複数)が含まれていると除外する MAIN.prototype.excludeLists = function(lists_base , nums){ if(!nums || !nums.length){return lists;} var lists = JSON.parse(JSON.stringify(lists_base)); for(var i=0; i<nums.length; i++){ var a = lists.indexOf(nums[i]) if(a !== -1){ lists.splice(a,1); } } return lists; }; MAIN.prototype.getComReach = function(emptyLists){ var lists = this.checkReach(emptyLists , 1); if(!lists){return;} return this.getComPlace(lists); }; MAIN.prototype.getUserReach = function(emptyLists){ var lists = this.checkReach(emptyLists , 0); if(!lists){return;} return this.getComPlace(lists); }; // リーチ箇所を特定して、終了するマスを一覧で返す MAIN.prototype.checkReach = function(emptyLists , mark){ if(this.buffer.length < 3){return;} var fixes = []; for(var i=0; i<win_pattern.length; i++){ var flg = []; for(var j=0; j<win_pattern[i].length; j++){ if(this.containNum_player(mark , win_pattern[i][j])){ flg.push(win_pattern[i][j]) } } if(flg.length === 2){ var nums = this.excludeLists(win_pattern[i] , flg); if(!this.containNum(nums[0])){ fixes.push(nums[0]); } } } return fixes; }; new LIB().construct(); })();

デモ

not canvas

まとめ

前回までのバグも含めて、無駄な繰り返し処理は、計算式にまとめて置きました。 座標をマス毎に処理していたのを1計算式にしたので、効率1/9になってますが、処理速度は変わりません・・・orz 今回、三目並べを作って思ったことは、ゲームアルゴリズム自体は、AIを求めなければさほど難しくないのですが、canvasでの描画をする場合に、通常のHTMLのDOM操作ではなく、canvas内でイベント処理(hoverやclickなどを)を座標判定しなければいけない点は、非常に作業がもどかしく感じました。 unityなどのライブラリを使えば、さほど考えなくても処理できるかもしれませんが、ブラックボックス処理で、速度が犠牲になるのは嫌なので、自分ライブラリを作っておかないとダメだろうと、思考を向かせてくれました。 あと、今回はアニメーション処理を全くやっていないのですが、gif画像を貼り付けて、webサイト内で随時動いてくれる処理なども、canvasを使うと自分でsetIntervalで行わなければいけないという事が分かり、これはもはや、webページを作っている感覚とはえらいかけ離れている感覚を覚えました。 ゲームプラットフォームの開発をやっていた感覚を思い出しますね。 でも、改めてcanvasは、guiディスプレイとして、今ではスマートフォンから、PCブラウザ、他の端末でのブラウザでも採用していないケースのほうが少ないようなので、これをメインフレームとして、使うという事は、魅力的かもしれませんね。 もっと精進したいと思います。

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。