yucatio@システムエンジニア

趣味で作ったものいろいろ

解いた結果をダイアログで表示する (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

KATAMINOが解き終わったときに、解けたか解けなかったかをダイアログに表示します。 ダイアログにはbootstrapのmodalコンポーネントを使用します。

ダイアログパーツの作成

Modal · Bootstrap に載っている例を参考にダイアログのhtmlを組み立てます。

はじめに、解けた場合のダイアログを作成します。javascriptの読み込みタグの直前にダイアログ用のhtmlを書きます。ヘッダーの背景色を黄色に変更しました。

index.html

<! -- 前略 -->
    </div><!-- end of container -->

    <! -- 追加ここから -->
    <div id="solved-modal" class="modal" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header bg-warning">
            <h5 class="modal-title">できたよ</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <p>こたえをみてみよう</p>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">とじる</button>
          </div>
        </div>
      </div>
    </div>
    <! -- 追加ここまで -->

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<! -- 後略 -->

同様に、今追加した部分のすぐ下に、解けなかった場合のダイアログ用のhtmlを書きます。ヘッダーの背景色は灰色にします。

index.html

    <! -- 追加ここから -->
    <div id="not-solved-modal" class="modal" tabindex="-1" role="dialog">
      <div class="modal-dialog" role="document">
        <div class="modal-content">
          <div class="modal-header text-white bg-secondary">
            <h5 class="modal-title">この くみあわせでは できないみたい</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
              <span aria-hidden="true">&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <p>ほかの くみあわせで ためしてみてね</p>
          </div>
          <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-dismiss="modal">とじる</button>
          </div>
        </div>
      </div>
    </div>
    <! -- 追加ここまで -->

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<! -- 後略 -->

ダイアログを表示するプログラム

ダイアログを表示する処理を書きます。

ダイアログを表すjQueryオブジェクトに対してmodalメソッドを呼び出します。オプションに"show"または"hide"を渡すことによって表示・非表示を制御します。

js/display.js

const display = {
  // 前略

  updateResultMessage: ({solverState}) => {
    $("#solved-modal").modal(solverState === "solvedSuccess" ? "show" : "hide")
    $("#not-solved-modal").modal(solverState === "solvedFailed" ? "show" : "hide")
  },
}

stateManagerへの登録

solverStateが変化したときにupdateResultMessageを呼び出すようにします。

js/stateManager.js

const stateManager = {
  // 前略

  setSolverState: (solverState) => {
    state.solverState = solverState

    display.updateDraggablePieces(state)
    display.updateStartButtons(state)
    // 追加
    display.updateResultMessage(state)
  },

  // 後略
}

実行結果

実行結果です。解けたときに"できたよ"のダイアログ、解けなかったときに、"この くみあわせでは できないみたい"のダイアログが表示されました。

f:id:yucatio:20191017092211p:plain

f:id:yucatio:20191017092225p:plain

updateResultMessageが複数回呼ばれたときの対応について

updateResultMessageは今回はsolverStateが変化したときにしか呼ばれないので、今回はこれで動作します。しかし今後他のstateが変化したときにも呼ばれた際に、何度もダイアログが表示されてしまうというバグが発生します。その場合は、解いたあと一度ダイアログを閉じたら、その状態でいる間は再びダイアログを表示しないといった変更が必要になります。

以上で解いた結果をダイアログで表示することができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

KATAMINOを解くのが終了したときの遷移 (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

KATAMINOを解くのが終わったときに、解けた(solvedSuccess)または解けなかった(solvedFailed)へ状態を遷移させます。 解けたか解けなかったかはsolverの中でわかるので、コールバックを使用してstateを更新します。

終了時のコールバックの定義

表示用コールバックを定義したのと同様に、KATAMINOが解けたとき・解けなかったときのコールバックを定義します。

js/action.js

const action = {
  // 中略
  startSolve: () => {
    solver.init(state.targetPieces)

    solver.solve({
      onUpdatePieces: (placedPieces) => stateManager.setPlacedPieces(placedPieces),
      // 追加ここから
      onSolved: () => stateManager.setSolverState("solvedSuccess"),
      onNotSolved: () => stateManager.setSolverState("solvedFailed"),
      // 追加ここまで
    })

    stateManager.setSolverState("solving")
  },
}

終了時にコールバックを呼び出す

KATAMINOが解けたときと、解けなかったときにコールバックを呼び出します。

js/solver.js

const solver = {
  // 前略
  solve : (options) => {
    // 変更前
    // const {onUpdatePieces = (placedPieces)=>{},} = options
    // 変更後
    const {onUpdatePieces = (placedPieces)=>{}, onSolved = ()=>{}, onNotSolved = ()=>{}} = options

    if (solver.solverStack.length <= 0) {
      console.log("解けなかった")
      // 追加
      onNotSolved()
      return
    }

    // 中略

    if (nextUnPlaced.length <= 0) {
      console.log("完成")
      // 追加
      onSolved()
      return
    }

    // 後略
  },
  // 後略
}

ボタンの更新

KATAMINOを解くのを終了したときのボタンの表示を変更します。

解き終わったときは、 "ピースを選び直すボタン"を表示します。"ピースを選び直すボタン"は解くのが終わったときと、一時停止のときに表示されます。

スタートボタンはピースを選択しているときと解いているときのみ表示するように表示の条件を変更します。

js/display.js

const display = {
  updateStartButtons: ({solverState, targetPieces}) => {
    $("#start-button").prop(
      "disabled", solverState === "solving"
    ).toggle(
      // 変更前
      // targetPieces.length >= 3
      // 変更後
      (solverState === "selectPiece" && targetPieces.length >= 3) || solverState === "solving"
    )

    $("#more-piece-button").toggle(targetPieces.length < 3)
    // 追加ここから
    $("#reset-button").toggle(
      solverState === "solvedSuccess" || solverState === "solvedFailed" || solverState === "pause"
    )
    // 追加ここまで
  },
  // 後略
}

実行結果

実行結果です。解き終わったときにボタンが"ピースを選び直すボタン"に変化しました。

f:id:yucatio:20191017083314p:plain


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

KATAMINOを解いているときの表示 (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

フィールド上のピースの表示を行います。また、KATAMINOを解いているときのピースの選択エリアやボタンの部分も変更します。

フィールドに置かれているピースの表示

前回、表示に必要な情報をstate.placedPiecesに格納したので、今回は表示していきます。 placedPiecesの各オブジェクトには、pieceIdspinIdoffset(左上のマスからの距離)が格納されています。

f:id:yucatio:20191014165139p:plain

これをcssで表現するには、topleftプロパティを使います。 x方向にずらすのはtopプロパティを使用します。 y方向にずらすにはleftプロパティを使用します。 offsetにはずらすマス目の数が入っているので、実際にtopleftに指定する値は、 それぞれ、x * (1マスのピクセル数(今回は25px))y * (1マスのピクセル数(今回は25px))になります。 また、今回はフィールドの画像の左上からマス目の左上まで距離があるので、そのぶんのピクセル数も加算する必要があります。フィールドの周りの余白は上左26pxです。

f:id:yucatio:20191014165206p:plain

1マスの幅やフィールドの余白の数値はconfigにまとめておきましょう。

js/config.js

// 追加ここから
const config = {
  cellSize: 25,
  fieldOffset: {top: 26, left: 26},
}
// 追加ここまで

フィールドにピースを表示する関数display.updateFieldPieceを実装します。 placedPiecesそれぞれについてpieceIdspinIdから対応する画像を取得したあと、top

updateFieldPieceはフィールドが更新されるたびに呼ばれるので、最初に全てのピースを非表示しておく必要があります。

js/display.js

const display = {
  // 前略

  updateFieldPieces : ({placedPieces}) => {
    $(".katamino-piece").hide()

    placedPieces.forEach((piece) => {
      $("#piece_" + piece.pieceId + "_" + piece.spinId
      ).css("top", piece.offset.x * config.cellSize + config.fieldOffset.top
      ).css("left", piece.offset.y * config.cellSize + config.fieldOffset.left
      ).show()
    })
  },
}

stateMangerupdateFieldPieceを登録します。

js/stateManger.js

const stateManager = {
  // 中略
  setPlacedPieces: (placedPieces) => {
    state.placedPieces = placedPieces

    // console.log("state.placedPieces", state.placedPieces) は削除してもよい
    // 追加
    display.updateFieldPieces(state)
  },
}

ここまでの実行結果

実行結果です。解いている過程が表示されました。画像になったので見やすくなりました。

f:id:yucatio:20191014175137p:plain

ピースの選択部分とボタンの挙動

ピースの選択部分と、ボタンの表示の挙動は下記のリンク先に書いてある通り、以下のような動作をします。

  • ピースの選択部分 : ピースを動かせないようにする
  • ボタン : スタートボタンを押せない状態で表示する

yucatio.hatenablog.com

ピースの選択部分の更新

ピースの選択部分のピースは、ピースの選択中(selectPiece)の時のみドラッグ可能です。ドラッグ要素のdraggable関数に""enanble"または"disable"を指定することでドラッグ可能かどうかを切り替えます。

js/display.js

const display = {
  // 中略

  updateDraggablePieces: ({solverState}) => {
    $(".draggable-piece").draggable(solverState === "selectPiece" ? "enable" : "disable")
  },
}

stateManager.setSolverStateに作成した関数を登録します。

js/stateManager

const stateManager = {
  setSolverState: (solverState) => {
    state.solverState = solverState

    // console.log("state.solverState", state.solverState) は削除してもよい
    // 追加
    display.updateDraggablePieces(state)
  },

  // 中略
}

ボタンの更新

解いている最中のボタンは、スタートボタンを押せない状態(disabled)にします。スタートボタン要素にprop("disabled", ...) を追加します。

js/display.js

const display = {
  // 仮引数を変更
  updateStartButtons: ({solverState, targetPieces}) => {
    // prop("disabled", ...) を追加
    $("#start-button").prop(
      "disabled", solverState === "solving"
    ).toggle(
      targetPieces.length >= 3
    )
    $("#more-piece-button").toggle(
      targetPieces.length < 3
    )
  },
  // 後略
}

solverStateが変更された際にボタンの再描画を行うようにします。

js/stateManager

const stateManager = {
  // 前略

  setSolverState: (solverState) => {
    state.solverState = solverState

    display.updateDraggablePieces(state)
    // 追加
    display.updateStartButtons(state)
  },

  // 後略
}

実行結果

実行結果です。解いている最中はピースの移動が出来なくなりました(画像ではわかりませんが)。また、スタートボタンが押せなくなっています。

f:id:yucatio:20191014175156p:plain

以上でKATAMINOを解いているときの表示ができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

KATAMINOを解くのをスタートさせる (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

スタートボタンを押したときの挙動をプログラミングしていきます。

actionの登録

スタートボタンへactionを登録します。スタートボタンがクリックされたときにaction.startSolveを呼びます。この関数はすぐ下で実装します。

js/main.js

// 前略

const initializer = {
  setEvent: () => {
    // 中略

    // 追加ここから
    $("#start-button").on("click", () => {
      action.startSolve()
    })
    // 追加ここまで
  }, 
}

actionの実装

action.startSolveを実装します。KATAMINOを解くのをスタートさせ、状態をsolving(解いている)に変更します。

js/action.js

const action = {
  // 中略

  // 追加ここから
  startSolve: () => {
    solver.init(state.targetPieces)

    solver.solve()

    stateManager.setSolverState("solving")
  },
  // 追加ここまで
}

画面の状態の変更

stateManager.setSolverStateを定義します。

js/stateManager.js

const stateManager = {
  // 中略

  // 追加ここから
  setSolverState: (solverState) => {
    state.solverState = solverState
    
    // TODO displayの呼び出し
    console.log("state.solverState", state.solverState)
  },
  // 追加ここまで
}

表示するピースのデータ形式の変更

STEP1では、kataminoFieldを描画用関数に渡し、kataminoFieldのマスをテーブルの1セルに対応させて描画していました。STEP2ではそれをやめ、スピンの画像を表示する方法に変更します。そのためには、ピースのIDとスピンのID、位置を渡す必要があります。位置としてoffsetを渡します。offsetはピースの左上が、フィールドの左上からどのくらい離れているかを表します。

f:id:yucatio:20191014140006p:plain

フィールドに置かれているピースのIDとスピンのID、オフセットを格納した配列(placedPieces)をスタックに追加します。 ピース新たに置かれたら、配列の最後にそのピースのIDとスピンのID、オフセットを追加します。

js/solver.js

const solver = {
  // 中略
  solverStack : [], 

  init : (targetPiece) => {
    const kataminoField = new Array(5).fill().map(() => (
      new Array(targetPiece.length).fill(-1)
    }

    solver.solverStack  = []
    const minEmpty = {x:0, y:0}
    // 追加
    const placedPieces = []

    targetPiece.forEach((pieceId) => {
      KATAMINO_ARR[pieceId].forEach((spin, spinId) => {
        // placedPiecesを追加
        solver.solverStack.push({kataminoField, minEmpty, pieceId, spinId, spin, unPlacedPiece: targetPiece, placedPieces,})
      })
    })
  },

  solve : () => {
    // 中略

    // placedPiecesを追加
    const {kataminoField, minEmpty, pieceId, spinId, spin, unPlacedPiece, placedPieces,} = solver.solverStack.pop()

    // 中略

    console.log("ピースが置ける")

    const nextField = util.copyArrayOfArray(kataminoField)
    solver.placeSpin(nextField, spin, offset, pieceId)
    console.log("nextField", nextField)

    const nextUnPlaced = unPlacedPiece.filter(id => id !== pieceId)
    console.log("nextUnPlaced", nextUnPlaced)

    // 追加ここから
    const nextPlacedPieces = [...placedPieces, {pieceId, spinId, offset}]
    console.log("nextPlacedPieces", nextPlacedPieces)
    // 追加ここまで

    // 表示用オブジェクトとしてnextPlacedPiecesを渡すように次で変更する
    display.show(nextField)

    // 中略

    nextUnPlaced.forEach((nextPieceId) => {
      KATAMINO_ARR[nextPieceId].forEach((nextSpin, nextSpinId) => {
        // placedPieces: nextPlacedPiecesを追加
        solver.solverStack.push({kataminoField: nextField, minEmpty: nextEmpty, pieceId:nextPieceId, spinId:nextSpinId, spin: nextSpin, unPlacedPiece: nextUnPlaced, placedPieces: nextPlacedPieces,})
      })
    })
    setTimeout(solver.solve, 300)
  },
  // 後略
}

表示用コールバックの作成と呼び出し

STEP1で作成したsolverは、solve関数の中でdisplay.showを呼び出していましたが、プログラムの構成の変更にともない呼び出し方を変更します。 stateManagerの呼び出しはactionから行う必要があるので、solverにコールバックを渡します。フィールドに置かれたピースが変更された場合にはsolverからactionのメソッドを呼び出すようにします。(stateManager.setPlacedPiecesはまだ定義していません)

js/action.js

const action = {
  // 中略
  startSolve: () => {
    solver.init(state.targetPieces)

    // 変更ここから
    solver.solve({
      onUpdatePieces: (placedPieces) => stateManager.setPlacedPieces(placedPieces),
    })
    // 変更ここまで

    stateManager.setSolverState("solving")
  },
}

solve関数を変更してフィールドが変更された時にコールバックを呼び出します。

js/solver.js

const solver = {
  // 前略
  solve : (options) => {  // 変更
    // 追加
    const {onUpdatePieces = (placedPieces)=>{}} = options

    // 中略

    const nextPlacedPieces = [...placedPieces, {pieceId, spinId, offset}]
    console.log("nextPlacedPieces", nextPlacedPieces)

    // display.show(nextField) を削除
    // 追加
    onUpdatePieces(nextPlacedPieces)

    // 中略
  },
  // 後略
}

solve関数の引数が追加されたので、setTimeoutで呼び出しているsolver.solveにも引数を追加します。そのままでは引数は与えられないので、無名関数でラップします。

js/solver.js

const solver = {
  // 中略
  solve : (options) => {
    // 中略
    if (! solver.isAllEmpty(kataminoField, spin, offset)) {
      // フィールドの外か、すでにピースが置かれている
      console.log("フィールドの外か、すでにピースが置かれている")
      // 変更
      setTimeout(() => solver.solve(options), 0)
      return
    }

    // 中略

    if (! solver.hasAllFiveTimesCells(nextField, nextEmpty)){
      console.log("フィールドが5の倍数以外で分断されている")
      // 変更
      setTimeout(() => solver.solve(options), 300)
      return
    }

    // 中略

    // 変更
    setTimeout(() => solver.solve(options), 300)
  },
  // 中略
}

stateへのフィールドに置かれているピースの追加

フィールドに置かれているピース(placedPieces)は描画に必要となるため、stateに登録します。

js/state.js

const state = {
  // One of "selectPiece", "solving", "solvedSuccess", "solvedFailed", "pause"
  solverState: "selectPiece",
  targetPieces: [1, 2, 9, 5, 10],
  // 追加
  placedPieces: [],
}

フィールドに置かれているピースの更新

placedPiecesを更新するsetPlacedPieces関数をstateManagerに作成します。中身は仮実装です。

js/stateManager.js

const stateManager = {
  // 中略

  // 追加ここから
  setPlacedPieces: (placedPieces) => {
    state.placedPieces = placedPieces

    // TODO displayの呼び出し
    console.log("state.placedPieces", state.placedPieces)
  },
  // 追加ここまで
}

実行結果

ここまでの実行結果です。 ピースが置かれたときに、stateManager.setPlacedPiecesが呼ばれ、フィールドに置かれているピースが表示されることが確認できました。

f:id:yucatio:20191014142958p:plain

ページが長くなってしまったので、一旦ここで区切ります。ピースの表示は次回行います。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

使うピースの選択を実装する (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

プログラミングをしていきます。まずは使うピースの選択です。

状態の追加

選択中のピースをstateに追加します。 初期値はpieceIdが1, 2, 9, 5, 10ですので、これを追加します。

js/state.js

const state = {
  // One of "selectPiece", "solving", "solvedSuccess", "solvedFailed", "pause"
  solverState: "selectPiece",
  // 追加
  targetPieces: [1, 2, 9, 5, 10],
}

state.targetPiecesの初期値は、初期化処理でhtml要素から取得した方がよいのですが、今回は簡易的にstateに直接書き込みました。

actionの登録

ピースがドロップされたときの動作を実装します。各エリアにドロップされたときに使うピースを更新します。

ドロップされたときの動作はdroppable関数のdropプロパティに記載します。使わないピースのエリア#unused-piece-droppableにドロップされたときは、ドロップされたピースを使うピースから削除するaction.removeFromTargetPieces(まだ定義していません)を呼び出します。同様に使うピースのエリア(#used-piece-droppable)にドロップされたときは、ドロップされたピースを使うピースに追加するaction.addToTargetPieces(まだ定義していません)を呼び出します。

ピースのIDは、 <span id="piece_0" class="draggable-piece m-2 p-0" data-piece-id="0">のように書いたhtmlタグの、data-piece-id="0"の部分から取得します。取得した際は文字列型なので、数値型に変換します。

js/main.js

// 前略

const initializer = {
  setEvent: () => {
    $(".draggable-piece").draggable({
      revert: "invalid"
    })

    $("#unused-piece-droppable").droppable({
      hoverClass: "bg-light",
      accept: ".draggable-piece",
      // 追加ここから
      drop : ((e, ui) => {
        const pieceId = ui.draggable.data("piece-id")
        action.removeFromTargetPieces(parseInt(pieceId, 10))
      }),
      // 追加ここまで
    })

    $("#used-piece-droppable").droppable({
      hoverClass: "hover",
      accept: ".draggable-piece",
      // 追加ここから
      drop : ((e, ui) => {
        const pieceId = ui.draggable.data("piece-id")
        action.addToTargetPieces(parseInt(pieceId, 10))
      }),
      // 追加ここまで
    })
  },
}

actionの実装

ドロップされたピースを使うピースから削除するaction.removeFromTargetPiecesとドロップされたピースを使うピースに追加するaction.addToTargetPiecesを実装します。

先にaction.removeFromTargetPiecesを実装します。受け取ったpieceIdが、使うピース(state.targetPieces)に含まれているか判定します。ピースが含まれていれば、そのピースの番号を使うピースのリストから削除します。新しいリストをstateManagerに渡します。 受け取ったpieceIdが、使うピースに含まれているかはindexOfの値が0以上かどうかで判定します。選択したピースのリストからの削除にはfilterを使用しています。 stateManager.setTargetPiecesはまだ実装していません。

ja/action.js

// 追加ここから
const action = {
  removeFromTargetPieces: (pieceId) => {
    if (state.targetPieces.indexOf(pieceId) >= 0) {
      const newPieces = state.targetPieces.filter(id => id !== pieceId)
      stateManager.setTargetPieces(newPieces)
    }
  },
}
// 追加ここまで

次に、action.addToTargetPiecesを実装します。受け取ったpieceIdが、使うピース(state.targetPieces)に含まれているか判定し、含まれいなければ、pieceIdを追加した配列をstateManager.setTargetPiecesに渡します。(この関数はまだ定義していません)

js/action.js

const action = {
  // 中略
  // 追加ここから
  addToTargetPieces: (pieceId) => {
    if (state.targetPieces.indexOf(pieceId) < 0) {
      stateManager.setTargetPieces([...state.targetPieces, pieceId])
    }
  },
  // 追加ここまで
}

stateManagerの実装

stateManager.setTargetPiecesを実装します。stateを書き換えます。(stateManagerのみがstateを書き換えることができます。) 書き換えたあとは画面を更新するのですが、一旦ここでコンソールに表示してここまでの動作を確認しましょう。

js/stateManager.js

// 追加ここから
const stateManager = {
  setTargetPieces: (targetPieces) => {
    state.targetPieces = targetPieces

    // TODO displayの呼び出し
    console.log("state.targetPieces", state.targetPieces)
  },
}
// 追加ここまで

ここまでの実行結果

実行結果です。"使うピース"の中にあるピースがstate.targetPiecesと同期しているのが分かります。

使わないピースを使うピースのエリアにドロップする↓ 6番のピースをドロップすると、targetPiecesの末尾に番号が追加されました。

f:id:yucatio:20191013131857p:plain

使うピースを使わないピースにのエリアにドロップする↓ 5番ののピースをドロップすると、targetPiecesから番号が削除されました。

f:id:yucatio:20191013131918p:plain

ボタンの表示の切り替え

ボタンの表示を切り替えます。選択されたピースの個数が3未満の場合には"つかうピースをもっとおいてください"のボタンを表示します。3以上の場合はスタートボタンを表示します。ボタンの表示の切り替えはtoggle関数で行います。

js/display.js

const display = {
  // show プロパティは削除

  // 追加ここから
  updateStartButtons: ({targetPieces}) => {
    $("#start-button").toggle(
      targetPieces.length >= 3
    )
    $("#more-piece-button").toggle(
      targetPieces.length < 3
    )
  },
  // 追加ここまで
}

使うピースが更新されたときに、ボタンの更新を呼び出すようにstateManagerを更新します。

js/stateManager.js

const stateManager = {
  setTargetPieces: (targetPieces) => {
    state.targetPieces = targetPieces

    // console.log("state.targetPieces", state.targetPieces) は削除してもよい
    // 追加
    display.updateStartButtons(state)
  },
}

実行結果

実行結果です。

使うピースが3個以上の場合↓

f:id:yucatio:20191013132123p:plain

使うピースが3個未満の場合↓

f:id:yucatio:20191013132140p:plain

以上で使うピースの選択を実装ができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

画面の状態を考える (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

画面の状態遷移について考えます。画面の状態とは、"ピースの選択中"や"解いている"という状態です。

"ピースの選択中"と"解いている"、"解けた"、"解けなかった"の状態遷移は以下のようになります。

f:id:yucatio:20191012221340p:plain

今回は一時停止ができるので、その状態を追加します。

f:id:yucatio:20191012221354p:plain

各エリアの状態は以下の通りになります。

  • ピースの選択エリア
画面の状態 パーツの状態
ピースの選択中 ピースを動かせるようにする
解いている ピースを動かせないようにする。フィールドに置いてあるピースは非表示にする
解けた ピースを動かせないようにする。フィールドに置いてあるピースは非表示にする
解けなかった ピースを動かせないようにする
一時停止中 ピースを動かせないようにする
  • ボタン
画面の状態 パーツの状態
ピースの選択中 ピースが3つ以上選択されている場合はスタートボタン、そうでない場合はもっとピースをおくボタンを表示する
解いている スタートボタンを押せない状態で表示する
解けた ピースを選び直すボタンを表示する
解けなかった ピースを選び直すボタンを表示する
一時停止中 ピースを選び直すボタンを表示する
  • フィールド
画面の状態 パーツの状態
ピースの選択中 空のフィールドを表示する
解いている 解いている過程を表示する
解けた 解けた組み合わせを表示する
解けなかった 空のフィールドを表示する
一時停止中 解いている過程を表示する

画面の状態ごとにどのようなパーツを表示すれば分かりやすくなりました。

初期状態はピースの選択のなので、これをstate.jsに追加します。

js/state.js

const state = {
  // One of "selectPiece", "solving", "solvedSuccess", "solvedFailed", "pause"
  solverState: "selectPiece",
}

以上で画面の状態を考えることができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

プログラムの構造を考える (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

プログラムの内部設計をします。今回はjQueryを使用しますが、(jQueryのサンプルでよく見るような)コールバックを多用するとコードの見通しが悪くなるので、今回はReact+Reduxのプログラム構成を参考にして設計してみます。

React+Reduxでは、プログラムの状態はstateオブジェクトに保存します。描画を行うオブジェクト(display)は、stateの状態のみを参照して画面出力を行います。stateの更新はreducerのみが行います。今回のプログラムではreducerという名前ではなく、stateManagerという名前にしています。

ユーザの入力によってactionが呼ばれます。actionstateManagerを呼び出します。

大まかな構造はこのようになります。

f:id:yucatio:20191012113811p:plain

stateManagerを呼び出すことができるのはactionのみです。display呼び出すことができるのはstateManagerのみです。 STEP1で作成したsolverオブジェクトは、displayを直接呼び出していましたが、actionを通じて呼び出すように変更します。

上記の他に、設定を保持するconfigオブジェクトを追加します。

プログラムファイルの一覧は以下です。 katamino-arr.js, util.jsの変更はありません。solver.jsは表示のための処理を追加します。

ファイル名 役割 備考
main.js 初期設定を行う STEP1の内容を全て置き換える
state.js 状態を保持する 新規ファイル
action.js ユーザの入力とフィールドの状態変化を受け取る 新規ファイル
stateManager.js 状態の更新と画面の更新を指示する 新規ファイル
display.js 画面のパーツを描画する STEP1の内容を全て置き換える
katamino-arr.js KATAMINOのスピン情報を格納する
solver.js KATAMINOを解くプログラム
config.js 設定値を保持する 新規ファイル
util.js ユーティリティ関数

プログラムの関係はこのようになります。

新規ファイルは空ファイルを作成しておきましょう。 state.jsaction.jsstateManager.jsconfig.jsjsフォルダの下に作成します。

f:id:yucatio:20191012114250p:plain

index.htmlでこれらのファイルを読みこんでおきます。読み込み順に気をつけてください。

index.html

<!-- 前略 -->

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
    <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
    <!-- 追加ここから -->
    <script src="js/config.js"></script>
    <script src="js/util.js"></script>
    <script src="js/state.js"></script>
    <script src="js/display.js"></script>
    <script src="js/stateManager.js"></script>
    <script src="js/katamino-arr.js"></script>
    <script src="js/solver.js"></script>
    <script src="js/action.js"></script>
    <!-- 追加ここまで -->
    <script src="js/main.js"></script>
  </body>
</html>

以上でプログラムの大まかな設計とプログラムを書く準備がととのいました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com