6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

闘魂Elixir ── 三目並べ(Tic tac toe)をPhoenix.LiveViewの上で動かすことを楽しむ

Last updated at Posted at 2024-09-18

はじめに

三目並べ(Tic tac toe) (◯☓ゲーム)をPhoenix.LiveViewの上で作ってみます。

動機

動機を述べます。

ふと、そろそろ、たいがいでReactチュートリアル三目並べ(Tic tac toe)を理解したいと思いました。
通算で何度も(年に1回ずつ5年くらい?)チュートリアルに取り組みましたが、私にはどうも馴染めずすぐ忘れてしまう始末でした。

馴染めなかったのは言葉にすると、以下のことが挙げられます。

  • functionの中に、またfunctionがでてくるのが気持ち悪い (=> 中のfunctionは、名前付き関数のエントリポイントを定義しているものと見えるようになりました。そういうものだと思うようにしました)
  • 関数のカッコ付きが呼び出し、カッコが無いと関数ポインタみたいなものであることを理解できていませんでした
  • const [currentMove, setCurrentMove] = useState(0)がなかなか理解できなかった(いまはcurrentMoveが現在値で、setCurrentMoveが更新用の関数ポインタみたいなものだとみえるようになりました)

言葉にするとこんなところです。どうやって克服したかというと、とにかく「そういうものだ」と思うことにして、深く考えないで、とにかく写経を繰り返しました。読書百遍自ずからその意通ず、みたいな感じです。上の「こう思うようになりました」という理解もJavaScript界隈の専門家の方に言わせると若干間違っている解釈があるのかもしれません。それについては、よくはありませんが、三目並べのチュートリアルの100行程度のコードはソラで書けるようにはなったという事実はあります。

私が写経をどのくらい繰り返したかというと、一週間毎日時間をとって取り組んでみました。一週間のうち最初の3日くらいはタイムトラベルの前までであえて止めていました。タイムトラベルの前までを完璧に理解してその後にタイムトラベルを取り組みました。そうしたところ何も見ずにソラで書けるようになりました :tada::tada::tada:

そんなこんなで完全に理解しました。(もちろん! 例の意味です)

それでここからは私のクセです。趣味です。同じことをElixirという素敵な関数型言語を使ってやってみたい衝動を抑えきれなくなりました。

それでこの記事をしたためているわけです。

特に参考にしたドキュメントなど

特に参考にしたドキュメントなどを示します。

できあがり品

できあがり品(ソースコード)を以下に置いておきます。

使用したソフトウェアとそのバージョン

使用したソフトウェアとそのバージョンを示します。

  • Elixir 1.17.2 (compiled with Erlang/OTP 27)

私はmacOSを使っています。
以下の記事を参考にしてインストールしました。いつも参照しています。ありがとうーーーーッ!!! ございます。

プロジェクトの作成とセットアップ

mix phx.new tic_tac_toe --no-ecto
cd tic_tac_toe
mix setup

ソースコードを作る

ソースコードを作っていきます。楽しんでいきます。
Reactチュートリアルのご経験があれば、なんとなく似ているところがたくさんあると思います。懐かしい思いにきっとかられることと思います。

lib/tic_tac_toe/tic_tac_toes.ex

ゲームの勝者を判定するTicTacToe.TicTacToes.calculate_winner/1関数です。

lib/tic_tac_toe/tic_tac_toes.ex
defmodule TicTacToe.TicTacToes do
  def calculate_winner(squares) do
    lines = [
      [0, 1, 2],
      [3, 4, 5],
      [6, 7, 8],
      [0, 3, 6],
      [1, 4, 7],
      [2, 5, 8],
      [0, 4, 8],
      [2, 4, 6]
    ]

    lines
    |> Enum.reduce_while(nil, fn [a, b, c], nil ->
      if Enum.at(squares, a) && Enum.at(squares, a) == Enum.at(squares, b) &&
           Enum.at(squares, a) == Enum.at(squares, c) do
        {:halt, Enum.at(squares, a)}
      else
        {:cont, nil}
      end
    end)
  end
end

lib/tic_tac_toe_web.ex

プロジェクトを作ったときにはじめから存在するファイルに、後述するFunction Componentsを.はじまりでどこからでも使えるようにimportしておきます。

lib/tic_tac_toe_web.ex
  defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import TicTacToeWeb.CoreComponents
      import TicTacToeWeb.GameComponents # 追加
      import TicTacToeWeb.Gettext

lib/tic_tac_toe_web/components/game_components.ex

Function Componentsです。今回は、lib/tic_tac_toe_web/components/core_components.ex形式で作ってみました。SquareモジュールやBoardモジュールにわける作り方もあります。

lib/tic_tac_toe_web/components/game_components.ex
defmodule TicTacToeWeb.GameComponents do
  use Phoenix.Component

  attr :status, :string, required: true
  attr :squares, :list, required: true

  def board(assigns) do
    ~H"""
    <div>
      <div class="status"><%= @status %></div>
      <div class="board-row">
        <.square :for={index <- 0..2} index={index} content={Enum.at(@squares, index)} />
      </div>
      <div class="board-row">
        <.square :for={index <- 3..5} index={index} content={Enum.at(@squares, index)} />
      </div>
      <div class="board-row">
        <.square :for={index <- 6..8} index={index} content={Enum.at(@squares, index)} />
      </div>
    </div>
    """
  end

  attr :index, :integer, required: true
  attr :content, :string, default: nil

  def square(assigns) do
    ~H"""
    <button class="square" phx-click="square_click" phx-value-ref={@index}><%= @content %></button>
    """
  end

  slot :inner_block, required: true
  attr :entries, :list, required: true

  def ordered_list(assigns) do
    ~H"""
    <ol>
      <%= for index <- @entries do %>
        <li key={index}><%= render_slot(@inner_block, index) %></li>
      <% end %>
    </ol>
    """
  end
end

TicTacToeWeb.GameComponents.board/1では、:forを使って、3個ずつSquareを配置するように書いています。このオプションの説明は「Heex extension: special attributes」に書いてあります。

lib/tic_tac_toe_web/live/tic_tac_toe_live/index.ex

状態を管理しているところといいましょうか、ゲームのエントリーポイントとでもいいましょうか、司令塔みたいな役割をしているLiveViewモジュールです。

lib/tic_tac_toe_web/live/tic_tac_toe_live/index.ex
defmodule TicTacToeWeb.TicTacToeLive.Index do
  use TicTacToeWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    history = [List.duplicate(nil, 9)]
    squares = Enum.at(history, 0)

    {:ok,
     socket
     |> assign(
       history: history,
       squares: squares,
       current_move: 0,
       winner: nil,
       status: status(nil, 0)
     )}
  end

  @impl true
  def handle_event("square_click", %{"ref" => index}, socket) do
    index = String.to_integer(index)
    square_click(socket, index, Enum.at(socket.assigns.squares, index))
  end

  @impl true
  def handle_event("move", %{"ref" => move}, socket) do
    move = String.to_integer(move)
    squares = Enum.at(socket.assigns.history, move)
    winner = TicTacToe.TicTacToes.calculate_winner(squares)

    {:noreply,
     assign(socket,
       squares: squares,
       current_move: move,
       winner: winner,
       status:
         status(
           winner,
           move
         )
     )}
  end

  defp square_click(socket, _index, value_of_index) when value_of_index != nil,
    do: {:noreply, socket}

  defp square_click(socket, _index, _value_of_index) when socket.assigns.winner != nil,
    do: {:noreply, socket}

  defp square_click(socket, index, _value_of_index) do
    current_move = socket.assigns.current_move

    next_squares = List.replace_at(socket.assigns.squares, index, mark(current_move))

    next_current_move = current_move + 1
    next_history = Enum.slice(socket.assigns.history, 0, next_current_move) ++ [next_squares]
    new_winner = TicTacToe.TicTacToes.calculate_winner(next_squares)
    status = status(new_winner, next_current_move)

    {:noreply,
     assign(socket,
       history: next_history,
       squares: next_squares,
       current_move: next_current_move,
       winner: new_winner,
       status: status
     )}
  end

  defp mark(current_move) when rem(current_move, 2) == 0, do: "X"
  defp mark(_current_move), do: "O"

  defp status(winner, _current_move) when winner != nil, do: "Winner: " <> winner
  defp status(_winner, current_move), do: "Next Player: " <> mark(current_move)
end

lib/tic_tac_toe_web/live/tic_tac_toe_live/index.html.heex

ゲーム全体のレイアウトファイルです。

lib/tic_tac_toe_web/live/tic_tac_toe_live/index.html.heex
<div class="game">
  <div>
    <.board squares={@squares} status={@status} />
  </div>
  <div class="game-info">
    <.ordered_list
      :let={index}
      entries={Enum.with_index(@history) |> Enum.map(fn {_, index} -> index end)}
    >
      <.button phx-click="move" phx-value-ref={index}>
        <%= if index > 0, do: "Go to move ##{index}", else: "Go to game start" %>
      </.button>
    </.ordered_list>
  </div>
</div>

lib/tic_tac_toe_web/router.ex

パスの指定です。プロジェクトを作ったときにはじめから存在するファイルに追加します。

lib/tic_tac_toe_web/router.ex
  scope "/", TicTacToeWeb do
    pipe_through :browser

    get "/", PageController, :home
    live "/tictactoes", TicTacToeLive.Index, :index # 追加
  end

assets/css/app.css

CSSは、Reactチュートリアルから意味もわからず丸パクリです。

assets/css/app.css
* {
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
  margin: 20px;
  padding: 0;
}

h1 {
  margin-top: 0;
  font-size: 22px;
}

h2 {
  margin-top: 0;
  font-size: 20px;
}

h3 {
  margin-top: 0;
  font-size: 18px;
}

h4 {
  margin-top: 0;
  font-size: 16px;
}

h5 {
  margin-top: 0;
  font-size: 14px;
}

h6 {
  margin-top: 0;
  font-size: 12px;
}

code {
  font-size: 1.2em;
}

ul {
  padding-inline-start: 20px;
}

* {
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
  margin: 20px;
  padding: 0;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.board-row:after {
  clear: both;
  content: '';
  display: table;
}

.status {
  margin-bottom: 10px;
}
.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.game-info ol li {
  margin-bottom: 10px;
}

タイムトラベル機能のボタンの間隔が詰まっているようにみえたので、最後のこの部分だけは、カンで追加しました。

.game-info ol li {
  margin-bottom: 10px;
}

動かし方

迷わず動かしてみましょう。

mix phx.server

visit: http://localhost:4000/tictactoes

output.gif

実際に動かせます(お試しいただけます)

https://tic-tac-toe-icy-paper-4088.fly.dev/tictactoes
にアクセスしてみてください。

Fly.ioにデプロイしています。

そのうち止めると思いますので、アクセスできないときはそういうことだと思ってください。

Phoenixのプロジェクトは、Deploying on Fly.io の通りにやると簡単にデプロイできます。デプロイするために必要なファイルをコマンドで自動的に作ってくれます。めちゃくちゃ便利です! 感動します! 以下の記事が詳しいです!

さいごに

三目並べ(Tic tac toe) (◯☓ゲーム)をPhoenix.LiveViewの上で作ってみました。

Elixirで書くことのメリットは、値がすべてイミュータブルであることです。イミュータブルかそうでないかを意識したり、コードを書き分けたりする必要は一切ありません。

React、Phoenix LiveViewともに理解が深まったことを感じます。
同じことを異なる言語、異なるフレームワークで試してみるのも理解を深める手段のひとつなのかもしれません。

あなたも三目並べ(Tic tac toe)をお好きなプログラミング言語、フレームワークで作ってみてはいかがでしょうか。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?