DockerfileとDockerイメージ、Dockerコンテナの違いをJavaで例える

こんにちは。新しい技術を学ぶとき、概念を表す単語でつまづくブログ主です。Dockerの用語がよくわからなかったのですが、Javaに例えると少しはわかるようになったので書いておきます。

Docker用語とJava用語の対比

かなりざっくりですが、以下のようにDockerとJavaのファイルの種類を対応付けました。

Docker用語 java用語 説明
Dockerfile .javaファイル コンピュータへの命令を人が読めるように書いたもの
Dockerイメージ jarファイル Dockerfileまたは.javaファイルをDocker EngineまたはJava VMで実行できるようにまとめたファイル
Dockerコンテ Javaプロセス イメージやjarファイルを実行したもの

生成の関連図は以下のようになります。

Java

f:id:yucatio:20191110223824p:plain

Docker↓

f:id:yucatio:20191110223839p:plain

DockerfileはJavaの.javaファイル

Dockerfile.javaファイルとも、コンピュータへの命令を人が読みやすいように書いたファイルです。

これらのファイルの中では、Javaで他の人が作成したライブラリ(jar)を使用する(importする)ことができるように、Dockerでも他の人が作成したイメージを使用する(FROMコマンドを使用する)ことができます。

DockerイメージはJavaのjarファイル

Javaでは、.javaコンパイルして作成された.classファイルと関連するリソースをまとめたjarファイルを作成することができます。

同様に、DockerfileをビルドすることでDockerfileと関連するファイルをまとめたDockerイメージを作成することができます。Dockerイメージの実態はtarファイルのようです。

DockerイメージはDocker Hubに登録すると他の人が使用することができます。 これは、javamavenリポジトリにjarファイルを登録すると他の人が使えるのに似ています。

DockerコンテナはJavaのプロセス

javaコマンドで実行するクラス名を指定するとjarファイルからコードが読み込まれ、プログラムが実行されます。

Dockerではrunコマンドでイメージを指定するとそのイメージが読み込まれ、コンテナが起動します。コンテナを起動すると、例えばnginxのイメージであれば、nginxがコンテナ内で起動します。

両者に共通するのは、jarファイル、Dockerイメージとも読み込み専用で、一旦それらのファイルを作成すればどのマシン(OS)でも動くということです。

DockerとJavaの違い

DockerとJavaの大きな違いは、Dockerではコンテナ内で作業した後の状態を、イメージとして保存できる点です。JavaではjarファイルからJavaプロセスを作成してもその状態をjarファイルに戻すことはありません。

あとがき

Dockerの勉強を始めたとき、Dockerのイメージとコンテナの違いがわからず検索したところ、この投稿に行き当たりました。

terminology - In Docker, what's the difference between a container and an image? - Stack Overflow

ここに、

コンテナはイメージのインスタンス

と書かれていて、一気に理解が進みました。"インスタンス"という言葉にはJavaで馴染みがあったからです。

仮想化技術のDockerとプログラミング言語Javaを比べるのは少し無理があるかと思いましたが、新しい技術は既存の身近なものをとっかかりにしたほうが理解が進みます。自分の場合はJavaのおかげでDockerの理解が進みました。

Dockerの書籍はこちら↓


ピースを戻す表示を追加する (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

現在の実装では、フィールドに新しくピースが置かれたときにフィールドのピースを描画しています。 フィールドに置かれたビースを再描画する際、フィールドに置かれたピースが一気に置き換わる瞬間があります。

f:id:yucatio:20191027135006p:plain

このような描画は少し唐突に思うので、 以下のようにピースを"戻す"過程を表示すると、自然に見えます。

f:id:yucatio:20191027135020p:plain

今回はこの処理を追加します。

戻した状態をスタックに投入する

ピースを"戻す"表示をするために、戻した状態のフィールドをスタックに投入します。 ピースの種類を変えるたびに"戻す"ので、スタックに投入する際、最初に戻した状態をスタックに投入して、ピースのすべてのスピンを投入します。

"戻す"表示用のスタックデータのspinIdspinnullにします。

js/solver.js

const solver = {
  // 中略

  init : (targetPieces) => {
    // 中略

    targetPieces.forEach((pieceId) => {
      // 追加ここから
      solver.solverStack.push({kataminoField, minEmpty, pieceId, spinId:null, spin:null, unPlacedPiece: targetPiece, placedPieces})
      // 追加ここまで
      KATAMINO_ARR[pieceId].forEach((spin, spinId) => {
        solver.solverStack.push({kataminoField, minEmpty, pieceId, spinId, spin, unPlacedPiece: targetPiece, placedPieces})
      })
    })
  },

  solve : (options) => {
    // 中略

    nextUnPlaced.forEach((nextPieceId) => {
      // 追加ここから
      solver.solverStack.push({kataminoField: nextField, minEmpty: nextEmpty, pieceId:nextPieceId, spinId:null, spin: null, unPlacedPiece: nextUnPlaced, placedPieces: nextPlacedPieces})
      // 追加ここまで
      KATAMINO_ARR[nextPieceId].forEach((nextSpin, nextSpinId) => {
        solver.solverStack.push({kataminoField: nextField, minEmpty: nextEmpty, pieceId:nextPieceId, spinId:nextSpinId, spin: nextSpin, unPlacedPiece: nextUnPlaced, placedPieces: nextPlacedPieces})
      })
    })

    solver.timer = setTimeout(() => solver.solve(options), solver.speed)
  }

  // 後略
}

"戻した"状態の表示

spinnullのときに、フィールド上のピースを更新するように変更します。

js/solver.js

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

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

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

    // 追加ここから
    if (! spin) {
      onUpdatePieces(placedPieces)
      solver.timer = setTimeout(() => solver.solve(options), solver.speed)
      return
    }
    // 追加ここまで

    console.log("pieceId", pieceId)
    console.log("spinId", spinId)

    // 後略
  },
  // 後略
}

ここまでの実行結果

実行結果です。 ピースを戻すことによって、連続的に見えるようになりました。

f:id:yucatio:20191027141310p:plain

しかし1つのピースの配置が長く表示される場合が出てきてしまいました。2回以上同じ表示がされているためです。1度"戻す"表示をして、そのまま別のピースが置かれず、再度同じ表示がされています。

表示時間の制御

前回と同じ表示の場合は、すぐに次のスタックを呼ぶ出すように変更します。前回と同じ表示かは、state.placedPiecesと表示しようとするplacedPiecesの長さが同じかどうかで判定します。

const solver = {
  // 前略
  solve : (options) => {
    // 前略

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

    if (! spin) {
      // 変更ここから
      const timeout = state.placedPieces.length === placedPieces.length ? 0 : solver.speed
      onUpdatePieces(placedPieces)
      solver.timer = setTimeout(() => solver.solve(options), timeout)
      // 変更ここまで
      return
    }

    console.log("pieceId", pieceId)
    console.log("spinId", spinId)

    // 後略
  },
  // 後略
}

実行結果

実行結果です。フィールドに置かれたピースが等間隔で更新されるようになりました。

f:id:yucatio:20191027141701p:plain

以上でピースを戻す表示を追加することができました。

おわりに

以上でKATAMINOを解くプログラムのチュートリアルはおしまいです。おつかれさまでした!


★目次

yucatio.hatenablog.com

フィールドに置かれたピースを選択エリアから消す

★前回の記事

yucatio.hatenablog.com

ピースの表示をもう少し工夫してみましょう。 KATAMINOを遠いている最中、フィールドに置かれたピースをピースの選択エリアから消します。 これによって、ピースの選択エリアにあるピースを取ってフィールドに置いているような効果が得られます。

f:id:yucatio:20191025094600p:plain

displayの実装

フィールドに置かれたピースを削除するために、 state.placedPiecesを引数に追加します。 はじめに全てのピースを表示し、フィールドに置かれたピースを隠します。

const display = {
  // 前略

  // 引数の追加
  updateDraggablePieces: ({solverState, placedPieces}) => {
    $(".draggable-piece").draggable(solverState === "selectPiece" ? "enable" : "disable")

    // 追加ここから
    $(".draggable-piece").children("img").show()
    placedPieces.forEach((place) => {
      $("#piece_" + place.pieceId + ">img").hide()
    })
    // 追加ここまで
  },
  // 後略
}

stateManagerへの追加

stateManagerへ登録します。

js/stateManager.js

const stateManager = {
  // 中略

  setPlacedPieces: (placedPieces) => {
    state.placedPieces = placedPieces

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

ここまでの実行結果

ここまでの実行結果です。フィールドに置かれたピースはピースの選択画面から消えました。

しかし、使うピースのcardコンポーネントの高さがピースを出したり隠したりするたびに変わり、上下に揺れて見辛いです。 また、消したことにより、消したピース以外のピースが左右に揺れ、こちらも見辛いです。

f:id:yucatio:20191021224344p:plain

フィールドの高さの固定

使うピース/使わないピースのcardコンポーネントの高さを変化させないようにするため、 読み込み時の高さに固定します。 画像が読み込まれた後に処理を実行するので、$(window).on('load')のコールバックを使用します。

js/main.js

// 前略

$(window).on('load', () => {
  initializer.setLayout()
})

const initializer = {
  // 前略

  // 追加ここから
  setLayout: () => {
    $("#unused-piece-droppable").height($("#unused-piece-droppable").height())
    $("#used-piece-droppable").height($("#used-piece-droppable").height())
  },
  // 追加ここまで
}

ピースの位置の固定

ピースが左右にぶれないようにするため、 ピースの画像を囲んでいるspan要素の幅を、初期値で固定します。display: inline-block;を追加して、幅を指定できようにします。 また、ピースが上下にぶれないようにするため、vertical-align: top;を追加します。

css/main.css

.draggable-piece {
  z-index: 10;
  /* 追加ここから */
  display: inline-block;
  vertical-align: top;
  /* 追加ここまで */
}

js/main.js

// 前略

const initializer = {
  // 前略

  setLayout: () => {
    $("#unused-piece-droppable").height($("#unused-piece-droppable").height())
    $("#used-piece-droppable").height($("#used-piece-droppable").height())
    // 追加ここから
    $(".draggable-piece").each((index, piece) => {
      $(piece).width($(piece).width())
    })
    // 追加ここまで
  },
}

実行結果

実行結果です。cardコンポーネント、ピースともにぶれることがなくなりました。 選択されたピースをフィールドに移動しているように見せることができました。

f:id:yucatio:20191024084911p:plain

以上で使用中のピースを選択画面から消すことができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

ピースが置かれないフィールドをマスクする (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

現在のUIでは、解くのをスタートさせたときに、どの位置までKATAMINOが置かれるべきかわかりにくいです。今回はこれを改善します。

本物のKATAMINOでは区切りとなる位置にバーを置いて、どの範囲の中にピースを収めるのかをわかりやすくしています。 今回のアプリでは、ピースが置かれないフィールドを暗くして、置かれる場所をわかりやすくします。

マスクするためのdiv要素の追加

ピースが置かれない場所を暗くするため、今回は背景が半透明の灰色のdiv要素をフィールドの画像に重ねます。

index.html

<!-- 前略 -->
      <div class="card my-2">
        <div class="card-body">
          <div id="katamino-field" class="mx-auto p-0">
            <img src="img/field/field.jpg" width="352" height="177"/>
            <!--  追加 -->
            <div id="field-mask"></div>

            <img id="piece_0_0" class="katamino-piece" src="img/piece/piece_0_0.png" width="125px" />
            <img id="piece_0_1" class="katamino-piece" src="img/piece/piece_0_1.png" width="25px" />
<!-- 後略 -->

背景色と透明度を指定します。position: absoluteを指定して、topとleftを、親要素からの相対位置で指定できるようにします。

css/main.css

#field-mask {
  position: absolute;
  background-color: #666666;
  opacity: 0.7;
}

マスクの初期位置を指定します。以下の図のように指定します。

f:id:yucatio:20191017224628p:plain

css/main.css

#field-mask {
  position: absolute;
  /* 追加ここから */
  top: 26px;
  left: 151px;
  width: 175px;
  height: 125px;
  /* 追加ここまで */
  background-color: #666666;
  opacity: 0.7;
}

ここまでの実行結果

実行結果です。6から12の部分が暗くなりました。

f:id:yucatio:20191017223320p:plain

displayへの追加

マスクするフィールドは使用するピースの個数によって変わります。使用するピースの数が変わったときにマスクの幅を変える必要があります。マスクの幅は、(12 - 選択されたピースの数) * セルのピクセル数です。また、左からの位置(left)も変更する必要があります。左からの位置は選択されたピースの数 * セルのピクセル数 + offsetで表すことができます。

js/display.js

const display = {
  // 前略

  // 追加ここから
  updateFieldMask: ({targetPieces}) => {
    $("#field-mask").css(
      "left", targetPieces.length * config.cellSize + config.fieldOffset.left
    ).css(
      "width", (12 - targetPieces.length) * config.cellSize
    )
  },
  // 追加ここまで
}

stateManagerへの追加

stateManagerへ登録します。

js/stateManager.js

const stateManager = {
  // 中略

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

    display.updateStartButtons(state)
    // 追加
    display.updateFieldMask(state)
  },
  // 中略
}

実行結果

実行結果です。使用するピースを変更すると、その数に応じてマスクされる部分が変化します。

使うピースが4個のとき↓

f:id:yucatio:20191017223304p:plain

使うピースが7個のとき↓

f:id:yucatio:20191017223251p:plain

解いてる途中で、どこまで埋まるかわかりやすくなりました。

f:id:yucatio:20191017223239p:plain

以上でピースが置かれないフィールドをマスクすることができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

速度調整機能を実装する (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

現在300ms間隔で描画を行なっていますが、ピースの数が多い場合は若干遅く感じます。そこで速度調節機能を追加します。

速度設定の追加

速度の設定を追加しましょう。 下記の記事で5段階のスライダーを用意しました。

yucatio.hatenablog.com

5段階それぞれに対応した速度を設定します。値は適当に決めています。

js/config.js

const config = {
  cellSize: 25,
  fieldOffset: {top: 26, left: 26},
  // 追加
  speedList: [10, 50, 100, 200, 500],
}

初期値はhtmlからスライダーの値を読み込むのが良いのですが、今回は簡単のため、スライダーの初期値をconfigに記載します。

js/config.js

const config = {
  cellSize: 25,
  fieldOffset: {top: 26, left: 26},
  speedList: [10, 50, 100, 200, 500],
  // 追加
  defaultSpeedLevel: 3,
}

速度を格納する変数の追加

速度を格納する変数をします。初期値は、config.speedList[config.defaultSpeedLevel]です。 setTImeoutの引数を、定義した変数に変更します。

js/solver.js

const solver = {
  solverStack : [],
  timer: null,
  // 追加
  speed: config.speedList[config.defaultSpeedLevel],

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

    // 中略

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

速度を変更するための関数をsolverに定義します。

js/solver.js

const solver = {
   // 中略

   // 追加ここから
   setSpeed: (speed) => {
     solver.speed = speed
   },
   // 追加ここまで
}

これで速度を変更する準備が整いました。

actionの登録

スライダー(speed-range)にactionを登録します。(action.changeSpeedはまだ定義していません)。$('#speed-range').val()でスライダーの値を取得していますが、文字列型なのでparseIntで数値に変換しています。

js/main.js

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

    // 追加ここから
    $('#speed-range').on("change", () => {
      const speedLevel = $('#speed-range').val()
      action.changeSpeed(parseInt(speedLevel, 10))
    })
    // 追加ここまで
  },
}

actionの実装

action.changeSpeedを実装します。configから値を取り出して、solverにセットします。

js/action.js

const action = {
  // 中略

  // 追加ここから
  changeSpeed: (speedLevel) => {
    solver.setSpeed(config.speedList[speedLevel])
  },
  // 追加ここまで
}

実行結果

実行結果です。スライダーを動かすと、ピースを置く速度が変わりました。(画像では伝わらないので、実際に動かして確かめてみてください)

f:id:yucatio:20191017115022p:plain

以上で速度調整機能の実装ができました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

一時停止、再開機能を実装する (KATAMINOを解くプログラムを作成する)

★前回の記事

yucatio.hatenablog.com

一時停止、再開機能を実装します。 一時停止機能は、solver.solveで実行したsetTimeoutをキャンセル(clearTimeout)することで実現します。

timerの保存

setTimeoutをキャンセルするには、setTimeouotの戻り値が必要になります。solverのプロパティに保存するように変更しましょう。

js/solver.js

const solver = {
  solverStack : [],
  // 追加
  timer: null,

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

    solver.solverStack  = []
    // 追加
    timer = null

    // 中略
  },

  solve : (options) => {
    // 中略

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

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

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

    // 中略

    // 変更前
    // setTimeout(() => solver.solve(options), 300)
    // 変更後
    solver.timer = setTimeout(() => solver.solve(options), 300)
  },

  // 後略
}

timerのキャンセル

timerのキャンセルをする関数をsolverに定義します。clearTimeoutを使用します。

js/solver.js

const solver = {
  // 前略
  // 追加ここから
  stop: () => {
    clearTimeout(solver.timer)
  },
  // 追加ここまで
}

ここまででKATAMINOを解くのをストップさせる準備が整いました。

停止・再開機能のactionへの登録

停止・再開ボタンにactionを登録します。(action.pauseaction.resumeはまだ実装されていません)

js/main.js

// 前略
const initializer = {
  setEvent: () => {
    // 中略
    // 追加ここから
    $("#pause-button").on("click", () => {
      action.pause()
    })

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

停止・再開actionの登録

停止アクションでは、solverをストップさせ、pauseの状態へ遷移します。 再開アクションでは、solverをスタートさせ、solvingの状態へ遷移します。

js/action.js

const action = {
  // 中略

  // 追加ここから
  pause: () => {
    solver.stop()
    stateManager.setSolverState("pause")
  },

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

displayの実装

停止ボタンは画面の状態がsolvingのときのみ押せるようにします。 再開ボタンは画面の状態がpauseのときのみ押せるようにします。

js/display.js

const display = {
  // 前略
  // 追加ここから
  updatePauseResumeButton: ({solverState}) => {
    $("#pause-button").prop("disabled", solverState !== "solving")
    $("#resume-button").prop("disabled", solverState !== "pause")
  },
  // 追加ここまで
}

stateManagerへの登録

solverStateが変わったときにupdatePauseResumeButtonを呼び出します。

js/stateManager.js

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

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

  // 後略
}

実行結果

実行結果です。 KATAMINOを解いている間は、停止ボタンが有効になり、押すと解いている途中で止まります。再開ボタンを押すと再び動き出します。再開ボタンは停止中のみ有効になっています。

f:id:yucatio:20191017103834p:plain

リファクタリング

actionのなかで、solver.solveを、startSolveresumeで2回同じパラメータで呼び出しているので、1つにまとめましょう。

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"),
    // })

    // 変更後
    action.solve()

    stateManager.setSolverState("solving")
  },
  // 中略
  resume: () => {
    // 変更前
    // solver.solve({
    //   onUpdatePieces: (placedPieces) => stateManager.setPlacedPieces(placedPieces),
    //   onSolved: () => stateManager.setSolverState("solvedSuccess"),
    //   onNotSolved: () => stateManager.setSolverState("solvedFailed"),
    // })

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

以上で一時停止、再開機能が実装できました。


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

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

★前回の記事

yucatio.hatenablog.com

"ピースをえらびなおす"ボタンが押されたときの動作をプログラミングしていきます。

フィールドに置かれているピース(placedPieces)を空にして、画面の状態(solverState)をピースの選択中(selectPiece)に変更します。

actionの登録

"ピースをえらびなおす"ボタンが押されてたときに、action.newPieceSelectionを呼び出します。(この関数はまだ定義していません)

js/main.js

// 前略

const initializer = {
  setEvent: () => {
    // 中略
    // 追加ここから
    $("#reset-button").on("click", () => {
      action.newPieceSelection()
    })
    // 追加ここまで
  }, 
}

actionの実装

action.newPieceSelectionを実装します。フィールドに置かれているピース(placedPieces)を空にして、画面の状態(solverState)をピースの選択中(selectPiece)に変更します。

js/action.js

const action = {
  // 前略
  // 追加ここから
  newPieceSelection: () => {
    stateManager.setPlacedPieces([])
    stateManager.setSolverState("selectPiece")
  },
  // 追加ここまで
}

実行結果

実行結果です。"ピースをえらびなおす"ボタンを押すと、フィールド上のピースが消え、スタートボタンが表示され、ピースがドラッグアンドドロップできるようになっています。ピースの選択ができるようになり、他の組み合わせも試すことができるようになりました。

f:id:yucatio:20191017093918p:plain

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


★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com