「1ページプログラミング」シリーズ
プログラム初心者、初学者のための様々な解説は世の中に溢れていますが、意外と「実際につくってみた」とそのソースコードの公開は少なめで、非常に人気があります。
例えばWin32APIでWindowsでテトリス作ってみた、動画など、大好きです。
そこで、このブログでは様々なゲームや仕組みのプログラミングを1ページで行っていきます。
このシリーズの特徴
1ページプログラミングの特徴は一枚のファイルで、メインのプログラム、解説を全部含んでいる、ということです。javascript によるプログラムなら、html、css、jsを全部含んでいます。
必要なライブラリは出来るかぎりCDNから、その他必要な場合は全て同梱します。
またプログラムの基本は vanilla (pure) javascript のみを使います。
TypeScript などの alt JS を用いていません。
解説しきれない部分は、どういったワードで検索するとよりいいか書いてあります。
「冒頭から読み進めていくだけで処理と実装の流れを理解する」ことに焦点を当てています。
使い方
このコードをコピーして、自分のフォルダに保存します。(index.html等の名前で保存してください)
そしてそれをGoogle Chromeで開くだけです。
コードは解説も含んでおり、初学者のために書いているため、中級以上のプログラマが初学者を育成する場面でもご利用いただけます。
丸コピで面接等に出すのはやめましょう。免責事項として、そういった際の一切の責任を負いません、よろしくお願いします。
ゲームの仕様
今回はリバーシです。黒が先手、白が後手になります。
リバーシは全ての盤面ゲームの基礎であり、パズルゲームの基礎です。
打てる場所は薄ら色が変わり、マウスで触るとさらに色が変わりますので、クリックしていきましょう。
AI機能はありませんので、二人でやると楽しめます!
コード本体
初版: 2021.2.14
微修正: 2021.3.31
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
<!DOCTYPE html> <html lang="ja" dir="ltr"> <head> <meta charset="utf-8"> <!-- まず文字コード指定 --> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- viewport 指定 --> <script src="https://code.jquery.com/jquery-3.3.1.js"></script> <!-- jQueryをCDNから読み込みます。 --> <title>リバーシ</title> <style type="text/css"> *{box-sizing: border-box;} .board{ display: flex; flex-wrap: wrap; width: 320px; height: 320px;} .board div{ background-color: #2e8b57; transition: all .3s ease-in; width: 40px; height: 40px; border:1px solid black;} .board div.candidate{ cursor: pointer; background-color: green;} .board div.candidate:hover{background-color:#00ffff; } .board div.PL1{ background:black; } .board div.PL2{ background:white; } .messages{ border: 1px solid black; width: 320px; height: 60px; display: flex; justify-content: center; align-items: center;} </style> </head> <body> <div class="board"> </div> <div class="messages"> </div> <script> // ▼▼▼▼ 解説 ▼▼▼▼ // リバーシのプログラムです。 // 今回はやる事が多いので、盤面初期化関数をはじめ、$() で HTML と紐づける前に必要な関数を全て作成します。 // $() の内部で実行する理由 -> htmlのロードが終わらないと、DOMを取得できないからですね! // まず盤面状態を表現する定数を宣言します。実体はint(数値)ですが、定数にすることでよりわかりやすくなります。 NONE = 0 // 何も設定されていない状態をNONEとする(実体は0) PL1 = 1 // プレイヤー1の取得したマスをPL1とする(実体は1) PL2 = -1 // プレイヤー2の取得したマスをPL2とする(実体は-1) // 現在のプレイヤーを設定します。 // 初手はPL1 とします。 NOW_PL = PL1 // 盤面配列を用意します // 二次元配列に連想配列が入っている形です。 // この配列の取り回しで、色々便利にコードを書く事ができます。 // U_ とついているのは、この変数はプログラム全体からアクセスされるグローバル変数だからです。 // U_ とつけることで、明確に差別化します。 // G_ でないのは好みです。 U_board = [] // 盤面を作成し、盤面状態を保存します。 // この時セルにすぐにアクセスできるように、セルへの参照も一緒に保存します。 function initializeGameBoard(){ for( i = 0 ; i < 8*8 ; i++){ let target = $("<div number="+i+"></div>") // jQueryで要素を作成します。 $(".board").append(target) // ボードに追加します。 // そのnumberが x, y でいうとどこに当たるのか計算します。 // 横軸の位置を x とします。折り返し数で割ったあまりが現在の x です。 let x = i % 8 // 縦軸の位置を y とします。折り返し数で割って小数点以下を切り捨てると、 y です。 let y = parseInt(i / 8) // その縦列にはじめて入る時、x 用の配列を作成します。 if( x == 0 ){ U_board[y] = [] } // そもそもこの値はNONE(何も置いていない)です。 let value = NONE // しかしリバーシには初期駒があります。 if( x == 4 && y == 3 ){target.addClass("PL1"); value = PL1} // 黒 if( x == 3 && y == 3 ){target.addClass("PL2"); value = PL2} // 白 if( x == 3 && y == 4 ){target.addClass("PL1"); value = PL1} // 白 if( x == 4 && y == 4 ){target.addClass("PL2"); value = PL2} // 白 // target を 盤面配列に組み込むことで、盤面配列から盤面のUIを操作できます。 U_board[y][x] = {value: value, target: target, x: x, y: y} } } // UIにおいては、 class="PL1" の時先手の場所 (黒)、 class="PL2" の時、後手の場所 (白) です。 // リバーシはどこにでもおける訳ではありません。 // 置ける場所を判定したり、置いたあとどれをひっくり返すかを判断するために、判定関数が必要です。 // 様々な実装方法があります。この関数の実装方法の工夫がリバーシプログラムの面白いところですね。 // あるマスから方向を与えると、その先を調べて、そのラインを調べます。 // ある(x,y)から (dx,dy) を与えるとその方向にサーチします。これは再帰的に呼び出されます。 // 「再帰」とはある条件で停止するまで、自分自身を呼び出すことです。 // 例えば一度上方向(x:0,y:-1)にサーチを開始すると、止まるまで繰り返し呼ばれて、その結果を全て返します。 // vは引き継いでいく値ですが、一番初めの呼び出しでは何も入れません。 function searchLine(x,y,dx,dy,v){ // v が空っぽの時、自分自身の場所を格納します。 if( typeof v == "undefined"){ // 初めのところに何もない場合、そもそもここで終了します。 if( typeof U_board[y][x] == "undefined"){ return [] } v = [U_board[y][x]] } // 検索先のx,yを取得します。 let nx = x + dx let ny = y + dy // 範囲外なので、vを返します。 if( typeof U_board[ny] == "undefined" || typeof U_board[ny][nx] == "undefined" ){ return v } if( U_board[ny][nx].value == NONE ){ // この場所には何もない // 何もない場合は2パターンあります。 if( v[0].value != v[v.length-1].value ){ // はじめの値と直前の値が異なる(はさめる) v.push(U_board[ny][nx]) } return v }else if( U_board[ny][nx].value != v[0].value ){ // 色が異なる場合 // 色が異なる場合は、引き続き検索します。 v.push( U_board[ny][nx] ) return searchLine( nx,ny, dx,dy, v ) }else if( U_board[ny][nx].value == v[0].value ){ // 色が同じ場合 // 色が同じ場合は、ここまでです。これまでの値を返します。 v.push( U_board[ny][nx] ) return v } } // ある点から全方向に検索し、そのラインを返します。 function searchAllLines(x,y){ let lines = [] let directions = [[-1,-1],[0,-1],[1,-1],[-1,0],[1,0],[-1,1],[0,1],[1,1]] // 全方向 for( let i = 0; i < directions.length; i++ ){ lines.push(searchLine(x,y,directions[i][0],directions[i][1])) } return lines } // 現在の盤面を精査して、置く事ができる場所を割り出す関数です。 // NOW_PLの値によって場所が変動します。 function searchBoardForCandidateCelles(){ let celles = [] // 8 x 8 の全ての盤面を精査します。 for( let i = 0; i < 8; i++){ for( let r = 0; r < 8; r++ ){ // 起点が現在のプレイヤーの色の場合のみ検索を開始します。 if( U_board[i][r].value == NOW_PL ){ let lines = searchAllLines( r, i ) for( s = 0; s < lines.length; s++){ let line = lines[s] if( line.length > 2 && // 3つ以上の値がないと挟めません line[line.length-2].value == -1 * NOW_PL && // 一つ前が他の色で line[line.length-1].value == NONE ){ // 最後のcellがNONEになるとき、そこはおける場所 celles.push( line[line.length-1] ) } } } } } return celles } // おける場所を割り出し、それらに演出を付与したいですね。 // class="candidate" を設定することで、候補色が変わるようにします。 // 新たにおかれる度に行います。 // いったん全ての演出を削除し、設定しなおします。 function setEventOnCandidateCelles(){ // 演出イベントを初期化 $(".board div").removeClass("candidate") // 通知イベントを初期化 $(".board div").off("click") let celles = searchBoardForCandidateCelles() for( let i = 0; i < celles.length; i++){ celles[i].target.addClass("candidate") // searchBoardForCandidateCelles は重複を削除していないため、複数のリスナーがセットされるのを防ぐ必要があります。 // 常に一つだけセットしたいので、毎回消しておきます。 celles[i].target.off("click") celles[i].target.click(function(e){ let x = celles[i].x let y = celles[i].y putPiece(x,y) }) } // 置けなくなった場合の判定に利用するため、返します。 return celles } function countCelles(){ let count_pl1 = 0 let count_pl2 = 0 // 8 x 8 の全ての盤面を精査します。 for( let i = 0; i < 8; i++){ for( let r = 0; r < 8; r++ ){ if( U_board[i][r].value == PL1 ){ count_pl1++ }else if( U_board[i][r].value == PL2){ count_pl2++ } } } return {pl1: count_pl1, pl2: count_pl2} } // 現在の状態をメッセージ枠に表示する関数を作成します。 function setMessages(){ let messages = [] // 手番 messages.push( ((NOW_PL==PL1)?"黒":"白")+"の手番です。" ) // 個数 let count = countCelles() messages.push( "黒: " + count.pl1 + " / 白: " + count.pl2 ) $(".messages").html(messages.join("<br>")) } // 勝敗の判定を行い、結果を表示します。 function showResult(candidate_celles){ let count = countCelles() if( count.pl1 + count.pl2 == 64 || candidate_celles.length == 0 ){ if( count.pl1 > count.pl2 ){ alert("プレイヤー1の勝利です。黒: "+count.pl1+", 白: "+count.pl2) }else if( count.pl2 > count.pl1 ){ alert("プレイヤー2の勝利です。黒: "+count.pl1+", 白: "+count.pl2) }else{ alert("引き分けです。") } } } // おける場所をクリックしたときの関数です。 function putPiece(x,y){ U_board[y][x].value = NOW_PL U_board[y][x].target.removeClass("PL1") U_board[y][x].target.removeClass("PL2") U_board[y][x].target.addClass(((NOW_PL==PL1)?"PL1":"PL2")) let lines = searchAllLines(x,y) for( let i = 0; i < lines.length; i++ ){ let line = lines[i] // 2つ以上のマスで、かつ最後のマスが現在プレイヤーのものであるとき、はさめています。 if( line.length > 2 && line[line.length-1].value == NOW_PL){ for( let v = 1; v < line.length; v++ ){ line[v].target.removeClass("PL1") line[v].target.removeClass("PL2") line[v].value = NOW_PL line[v].target.addClass(((NOW_PL==PL1)?"PL1":"PL2")) } } } // プレイヤーを入れ替えます。 NOW_PL = NOW_PL * -1 // もう一度おける場所を再計算します。 // おける場所の総数を返して、結果表示関数に渡します。 let celles = setEventOnCandidateCelles() // メッセージを表示します。 setMessages() // 勝敗を確定します。 showResult(celles) } $(function(){ // loadを待ってから諸々設定するため、 $() の中に入れます。 // 盤面を初期化します。 initializeGameBoard() // クリック可能なマスに演出と通知イベントを入れます。 setEventOnCandidateCelles() // メッセージを表示します。 setMessages() }) </script> </body> </html> |