上海麻雀は積み上げられた麻雀牌の中から同じ種類の牌を2個ずつ取り、画面上の全ての牌を取ればクリアとなる思考型パズルゲームです。取ることができるのは積まれた麻雀牌の左右のどちらかが空いている牌で、かつ上に牌が積まれていないものでなければなりません。

HTML部分

style.css

グローバル変数と定数

グローバル変数と定数を示します。

app.js

Tileクラスの定義

牌を表示させたり取り除けるかの判定をするためにTileクラスを定義します。

コンストラクタ

コンストラクタを示します。

牌の設置

三次元配列 tiles3x3 にTileオブジェクトが格納されている場合、その牌は後述のDraw関数によって描画されます。SetPosition関数はtiles3x3に自分自身を格納して、引数の位置に牌が描画されるようにするためのものです。

牌は選択できるか?

CanSelect関数はその牌が選択可能か(積まれた麻雀牌の左右のどちらかが空いている牌で、かつ上に牌が積まれていないという条件を満たすかどうか?)を返します。IsTop関数は自分自身の上に他の牌が積まれていないかどうかを返す関数です。

牌の描画

Draw関数は牌をcanvasに描画します。

アンドゥ機能

Undo関数は一度取り除いた牌を再度tiles3x3に格納して表示されるようにします。SetPosition関数実行時に設定されたRow, Leftなどの値は変化しないのでそのまま使います。

ページが読み込まれたときの処理

ページが読み込まれたときに行われる処理は以下のとおりです。

ローカルストレージにプレイヤー名が保存されているのであればテキストボックスにそれを表示させる
牌選択用のボタンを生成する(createSelectButtons関数)
$fieldのサイズ変更
問題の生成(createProblem関数)
更新処理の開始(update関数)
レンジスライダーでボリューム調整ができるようにする(initVolume関数)
SNSでの拡散を期待してXにポストするボタンを表示する(ご協力をお願いします m(_)m )

initVolume関数はここで解説しています。

牌を選択できるようにするために牌が描画されている位置に見えないボタンを設置します。createSelectButtons関数はそのボタンを生成する関数です(生成するだけでここでは追加はしない)。

問題の生成

問題を生成する処理を示します。言うまでもなく解法が存在する問題を出題しなければなりません。ただ困ったことに四川省の場合とちがってランダムに配置するだけでは解ける問題が生成される確率が低くなってしまうのです。

初期配置は以下のようになっています(春夏秋冬牌をいれるとややこしくなるので入れていません)。

これを三次元配列で表現すると以下のようになります。




初期配置を決めるときは同じ種類の牌をふたつずつセットしていきます。セットする場所は各行の外側から、そして見える部分からです。下の段は上の段にある牌が設置されないと設置されません。このようにしておけば設置していった順にとっていけば必ずゲームクリアすることができます。

initTiles関数で問題を生成するのですが、乱数を使った処理だとうまくできない場合があるので問題として成立しているかチェックしてチェックが通ったら牌選択用のボタンを設置してゲームを開始できるようにしています。

牌に番号をつけます。11~19は一萬から九萬、21~29は一筒から九筒、31~39は一索から九索、41~47は字牌(東南西北白発中)です。この値を乱数とセットにして配列 pairs に格納して乱数でソートしています。そしてペアになる牌を2つずつ生成して tiles に格納しています。

三次元配列を定義して牌の初期位置にあたる部分を undefined に、牌は存在しない部分を null で初期化します。そして三次元配列の各行を両外側から検索し、undefined の部分を探します。見つかったらそのなかから2
箇所ランダムに選んで同じ種類の牌をセットします。こうすることで解法が存在する問題が生成されます。ただときどき失敗するので最後に三次元配列のなかに undefined がないことを確認します。

getEdges関数は三次元配列の各行を外から検索して最初にみつかったundefinedの位置のリストを返します。ただしその上の段もundefinedである場合はその位置に先に牌を配置しなければならないので無視します。

getRandom2関数は 0以上でmax以下の異なる整数の乱数のペアを返します。返される整数はmaxも含みます。やっていることは2回乱数を生成して異なる整数であればそのままペアにして返し、同じものが生成された場合はその前後の整数をペアにして返しているだけです。

牌の選択用のボタンを設置する

setSelectButtons関数は選択することができる牌が描画されている位置に見えないボタンを設置するためのものです。このボタンをクリックすることで牌を選択する処理がおこなわれるようになります。

ゲーム開始時の処理

ゲーム開始時におこなわれる処理は以下のとおりです。

効果音の再生
問題の生成
selected = null とすることでどの牌も選択されていない状態にする
sugested = [] とすることでどの牌も提案されていない状態にする
やりなおし操作に関する情報をクリアする
isPlaying, isStalemate, isGiveuped の各フラグのクリア
プレイ開始時刻を現在の時刻とする
プレイヤー名に入力されている名前をローカルストレージに保存する
スタートボタンの非表示、ゲームをプレイするうえで必要な操作ボタンの表示

牌が選択されたときの処理

牌が選択されたときにおこなわれる処理は以下のとおりです。

ゲームスタート前のクリックは何もしないで警告音だけ鳴らす
探すコマンドでペアの提案がされている場合は解除
ペアになる片方の牌がまだ選択されていないときは現在選択されたものを selected に格納する
ペアになる片方の牌がすでに選択されている現在選択されたものと同じ種類の牌か調べる
同じ種類であれば消去し、異なる牌であれば警告音を鳴らす。選択状態にある牌も選択解除する

牌を消す処理は消える牌のある位置の tiles3x3[depth][row][col] を null にするともに selected = null にして選択状態を解除するのですが、その前にあとで元に戻せるように undo に tiles3x3[depth][row][col] を格納しておきます。そして牌が消えることで選択可能な牌も変わるので setSelectButtons関数を実行してボタンの位置を変更します。

また牌が消えたときにゲームクリアになっているかもしれないので確認の処理をおこないます。ゲームクリアでない場合はつぎに取ることができる牌のペアがあるか調べます。存在しない場合は手詰まりなので効果音を鳴らしてユーザーに知らせます。

ゲームクリア判定

ゲームクリアかどうかを調べる処理を示します。

3次元配列 tiles3x3 がすべてnullならすべての牌が取り除かれたことになるのでゲームクリアです。この場合はXへ結果をポストするボタンを表示させるとともに効果音の再生、isPlayingフラグのクリア、操作ボタンの非表示、スタートボタンの再表示をおこないます。

手詰まり判定

つぎに取ることができる牌のペアがあるか調べる処理を示します。

Mapに選択することができる牌を牌の番号をキーとして格納していき、同じキーが存在する場合はペアができるということなのでそのTileオブジェクトのペアを返します。存在しない場合はnullを返します。

キャンセルボタンがクリックされたときの処理

キャンセルボタンがクリックされたときは牌が選択状態にあるときや探すコマンドによって牌のペアが提案されているときはこれらを解除します。

そうではない場合は undo.length が 2 以上のときは消した牌を元に戻す処理をおこないます。undoの最後尾をふたつ取り出し、Tile.Undo関数を実行して、元の位置に戻します。そして選択可能な牌の位置が変わるので setSelectButtons関数を実行します。また手詰まりになっていた場合もこの操作で手詰まり状態が解除になるので isStalemate = false とします。

選択できない牌や牌がない部分をクリックした場合は警告音を鳴らすとともに、選択状態にあった牌を非選択の状態に変更します。

探すコマンドを選択したときの処理

探すコマンドを選択したときは消すことができる牌のペアを探して最初に見つかったものを sugested 配列に格納して強調表示させます。そして isGiveuped フラグを true に変更します。

更新と描画処理

更新処理を示します。

更新のたびにプレイ開始時刻からの経過時間を計算して playMilliseconds に格納します。また上部の説明文をゆっくり左にスライドさせます(howToPlayLeft を変化させることで文章の表示開始座標が変わる)。そのあとdraw関数で牌の描画処理をおこないます。

描画処理

描画処理は三次元配列 tiles3x3 を調べて nullでなければそのオブジェクトを描画します。このとき上の段にある牌をあとから描画したいので depth は大きい順に調べています。そのあとプレイ開始からの経過時間を描画します。このとき手詰まりならその旨も描画します。また探すコマンドを選択したあとは赤字で経過時間が描画されるようにしています。

getTimeText関数はミリ秒を◯分◯秒◯の形式で文字列に変換します。