はじめに
Elixir/Phoenixを使って、AIと対戦できる4目並べを作成してみました。
その1では、人対人で4目並べのゲームができるものを作成します。
ゲームのボードの表示方法
ゲームのボードは、キャンバスを使って自力で書く方法もありますが、以前オセロやマインスイーパーを作った時に、CSSのGridを使うと描画部分をいい感じにHTMLに任せられて楽だったので、この方法で作成してみます。
HTMLとCSSはつぎのような感じです
ボードのHTML
grid-template-columns, grid-teplate-rowsで正方形を5×5個並べています
各マスはマスの状態に応じたclassを割り当てています。
石を置いたり、ターンが変わるごとにこのclassの値を書き換えています。
この例は、(1,1)に白石があり、黒の手番の時です。
※各マスのdivは実際は長いので、(1,4)の10個分までになってますが、実際は25個あります。
CSS
| class | 役割 |
|---|---|
| cell | すべてのマスに適用。色などを決める |
| turn-black | マウスがおかれたら黒石を表示 |
| turn-white | マウスがおかれたら白石を表示 |
| black | 白石を表示 |
| white | 黒石を表示 |
.cell {
border: 1px solid black;
background: rgb(91, 184, 37);
}
.turn-white:hover {
background: rgb(104, 231, 30);
background-position: center;
background-image: url("../images/white.svg");
}
.turn-black:hover {
background: rgb(104, 231, 30);
background-position: center;
background-image: url("../images/black.svg");
}
.cell.white {
background-position: center;
background-image: url("../images/white.svg");
}
.cell.black {
background-image: url("../images/black.svg");
}
画面表示
マウスをあてたセルがハイライトされ、自分の色の石が表示されます。
マウスを移動させると、石も連動して移動して表示されます。
※マウスポインターがキャプチャできてませんが、左上の黒石にポインターがあります。
LiveViewでの表示
手番が変わる度に、HTMLを書き換えます。書き換える部分は、各マスに対応するdivのclassの値です。
classの値は、前述の石の状態により決まるclassです。このほかに、
セルをクリックした時に、セルの場所をイベントハンドラーに伝えるために、phx-value-x、phx-value-yの値を埋め込んでいます。
<%= for {y, x, class_string} <- create_elements(@board) do %>
<div class={class_string} phx-click="clicked" phx-value-x={x} phx-value-y={y}></div>
<% end %>
render関数全体は、次の通りになります。
def render(assigns) do
~H"""
<div style={"display: grid;
grid-template-columns: repeat(#{@board.size},#{@width/@board.size}px);
grid-template-rows: repeat(#{@board.size},#{@width/@board.size}px);
width: #{@width}px;
height: #{@width}px;"}>
<%= for {y, x, class_string} <- create_elements(@board) do %>
<div class={class_string} phx-click="clicked" phx-value-x={x} phx-value-y={y}></div>
<% end %>
</div>
<%= if @board.done do %>
<.button phx-click="new_game">New Game</.button>
<% end %>
"""
end
# ゲームの進行方法
石を置く処理
マス目をクリックすると、"clicked"のhandle_eventが呼び出されます。
:boardには、ゲームの状態が保存されているので、この値を、
Gomokuモジュールのputで更新します。
def handle_event("clicked", %{"x" => x, "y" => y}, socket) do
x = String.to_integer(x)
y = String.to_integer(y)
{:noreply, update(socket, :board, &Gomoku.put!(&1, {y, x}))}
end
ゲームのロジック
LiveViewを使って、画面表示部分はできあがりました。Gomokuモジュールにゲームのロジックを記述します。
put!()では、
boardの{y,x}キーの値を自分の手番の番号に更新
update_done()でゲームオーバかどうかの確認をして、手番の更新をしています。
すでに石が置かれている場所に石を置くと、ゲームオーバとなるロジックになっています。これは、このGomokuモジュールは、AIの学習にも使用していて、おける場所に置いた場合は、反則負けとする必要があったので、その関係でこうなってます。
def put!(%Gomoku{board: board, turn: turn} = gomoku, {y, x}) do
case board[{y, x}] do
0 ->
gomoku
|> struct(board: %{board | {y, x} => turn})
|> update_done({y, x})
|> turn_player()
_ ->
struct(gomoku, miss: true, done: true)
end
end
ゲームロジックの動作確認
newを実行すると、なにも石の置かれていない初期状態のボードの%Gomoku{}を返します。
Gomoku.new(5, 4)
%Gomoku{
board: %{
{0, 0} => 0,
{0, 1} => 0,
{0, 2} => 0,
{0, 3} => 0,
{0, 4} => 0,
{1, 0} => 0,
{1, 1} => 0,
{1, 2} => 0,
{1, 3} => 0,
{1, 4} => 0,
{2, 0} => 0,
{2, 1} => 0,
{2, 2} => 0,
{2, 3} => 0,
{2, 4} => 0,
{3, 0} => 0,
{3, 1} => 0,
{3, 2} => 0,
{3, 3} => 0,
{3, 4} => 0,
{4, 0} => 0,
{4, 1} => 0,
{4, 2} => 0,
{4, 3} => 0,
{4, 4} => 0
},
size: 5,
turn: 1,
done: false,
draw: false,
miss: false,
k: 4
}
put!()を使って石を置く操作ができます。(1,1)に白(先手)と(2,2)に黒を置いた状態を作ってみます。
でバック用に、render()を使うとテキストで盤面を表示できるようにしてみました。
Gomoku.new(5, 4)
|> Gomoku.put!({1, 1})
|> Gomoku.put!({2, 2})
|> Gomoku.render()
|> IO.puts()
abc
1: -----
2: -○---
3: --●--
4: -----
5: -----
まとめ
- 盤面の表示は、HTML/CSS、五目並べのゲームロジックはGomokuモジュールに分割して作成した。
- 組み合わせることで、人対人の対戦ができる状態になった。
【Elixir/Phoenix】対戦4目並べを作る(強化学習編)では、強化学習によりAIの対戦相手を作成する部分を説明します。
関連記事

