8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう!後編

Last updated at Posted at 2021-12-17

この記事は、「fukuoka.ex Elixir/Phoenix Advent Calendar 2021」の18日目になります。17日目は @pojiro さんの[ ]話でした。

東京だけど fukuoka.ex の YOSUKENAKAO.me です。
The Waggleという会社でScrumとElixirと研修講師をやってます。

Scrum開発で学ぶElixir研修を2020年にテストケースとしてローンチし、9名の方がわずか1ヶ月でElixirでモジュール開発から、簡単な地図アプリケーションをデプロイするまで成長しました。
企業向けの研修ですが、個人でも受けたいという方がいらしたらご連絡お待ちしてます。

ご要望が多ければ個人向けも提供したいと考えております。

なお、研修講師になりたい人も 絶賛募集中です。
こちらは、Elixirだけでなく、JavaやPHP、C、C#, Python、機械学習、統計などの分野でも募集しています。

自身のスキルの棚卸しや、コーチングやファシリテーションのスキルなども身につきますので、自身のスキルアップとしてチャレンジしたい方もいましたらこちらのページの下記にあるお問い合わせフォームまでご連絡ください。

この記事では、LiveViewを使って簡単にタイピングゲーム擬きを作成していきます。の後編です。
前編は、「Elixir Advent Calendar 2021」の15日目で掲載中です。
前編を見てない方はそちらからどうぞLiveViewを使って簡単にステートフルなタイピングゲームアプリを作ろう! 前編

ページリロードを抑制

Key入力をするたびにページリロードが入ると、忙しないので以下のハンドルイベント関数を追加し、単純にkeyを押しただけではページのリロードが起きないように変更します。

  # page-activeの時は何もしない
  def handle_event("page-active", %{}, socket) do
    {:noreply, socket}
  end

  # page-inactiveの時は何もしない
  def handle_event("page-inactive", %{}, socket) do
    {:noreply, socket}
  end

表示するワードのリストを追加

mount関数にwordをリストから追加するように準備します。

    def mount(_params, _session, socket) do
        data = ["Test Case", "Analyze the business issues.", "Grow as a Team.", "Develop a plan to solve the business issues."]
         {:ok, assign(socket, word: Enum.at(data, 0) )}
    end

シフト操作の処理

大文字変換をする際に"Shift"を押す必要があります。
"Shift"を押しながらkey入力する必要があるので、"Shift"を受け付けてもwordの更新をしないように
変更します。
ついでに、後々、誤入力の判定もしたいので、"Shift"以外で先頭の文字と一致しなかった場合のルートをOtherとして確保しておきます。

    @impl true
    def handle_event("typing", %{"key" => key, "word" => word, "char" => char}, socket) do

        word = if char == key do
            [_head | tail] = String.graphemes(word)
            List.to_string(tail) 
        else
            case key do
                "Shift" -> word
                _Other -> word
            end
        end

        {:noreply, assign(socket, word: word) }
    end

リスト情報の更新

ここで、wordの情報が最後の文字まで消えた後、次のリストの文字に更新をしたいので、カウンタを使って管理していきたいと思います。
そこで、Agentを使ってカウントを持たせてみます。

typing_game_web/util/counter.ex を作成します。
Agentについて詳しく知りたい方はこちらを読んでみてください。
今回は、長くなるので割愛します。

defmodule TypingGameWeb.Util.Counter do
    use Agent
    
    def start_link(initial_value) do
      {status, pid} = Agent.start_link(fn -> initial_value end, name: __MODULE__)
      case status do
        :ok -> pid
        :error -> "error"
      end
    end
    
    def value do
      Agent.get(__MODULE__, & &1)
    end
    
    def increment do
      Agent.update(__MODULE__, &(&1 + 1))
    end
    
    def stop(pid) do
      Agent.stop(pid)
    end
    
end

続いて、Agentを起動するタイミングを作りたいと思います。

mountに作成してしまうと、更新のタイミングで上書きされてしまうので、スタートボタンを作り、ボタンをクリックしたタイミングで作成するように変更していきます。

<section class="phx-hero">
  <div class="container"
    phx-window-focus="page-active"
    phx-window-blur="page-inactive"
    phx-window-keyup="typing"
    phx-value-word={ @word }
    phx-value-char={ String.at(@word, 0) }
    phx-value-data={ @data }
  >
  <%= if @word == "" do %>
   <button phx-click="start", phx-value-data={ @data }>開始する</button>
  <%= else %>
   <p><%= @word %></p>
  <% end %>
  </div>
</section>

これで、スタートボタンをクリックして、ゲームスタートができるといった表現ができました。では実際にそのように@wordが初期値 "" となるように変更していきましょう。

    def mount(_params, _session, socket) do
         {:ok, assign(socket, word: "" , data: [], count: 0 )}
    end

handle_event で start をキャッチして Counterを開始する部分を実装します。ここで、mountにあったdataのリストを実装する形に変更します。

    def handle_event("start", %{}, socket) do
        data = ["Test Case", "Analyze the business issues.", "Grow as a Team.", "Develop a plan to solve the business issues."]
        pid = Counter.start_link(0)
        {:noreply, assign(socket, word: Enum.at(data, 0), data: data,
                              count: Counter.value(), pid: pid
                              )}
    end

wordの文字列が1になったらカウントに1追加して、次のリストの文字列をwordにマッチさせる処理を追加して、カウンタの値を加算していきます。

本来は、0になったらの条件で進めたいのですが、現時点では0の判定にすると判定前に
エラーで終了してしまうので1の判定にしておきます。

1で判定の中に入ったら、Agentで管理してるカウンタを加算して
dataに束縛してる値から2つ目の要素を取り出してwordに再束縛します。
この時、dataはsocketにあるので、socket.assigns.dataで取得する
事が可能です。

次に、dataのリストの値とカウンタの値が同じになったら、Agentを止めたいので
その判定処理を入れておきます。

    @impl true
    def handle_event("typing", %{"key" => key, "word" => word, "char" => char}, socket) do
        
        word = if 1 == String.length(word) do
            Counter.increment()
            socket.assigns.data |> Enum.at(Counter.value())
        else  
            if char == key do
                [_head | tail] = String.graphemes(word)
                IO.inspect "#{String.length(word)}"
                List.to_string(tail)     
            else
                case key do
                    "Shift" -> word
                    "Enter" -> word
                    _Other -> word
                end
            end
        end

        if Enum.count(socket.assigns.data) == Counter.value() do
            Counter.stop(socket.assigns.pid)
        end

        {:noreply, assign(socket, word: word) }
    end

これで、一通り、タイピングゲーム擬きの形ができました。
あとは、これまでにやったことを応用して、タイプミスをカウントするAgentを用意して、_Other のケースでカウントアップしたり、data に取り込むリストをCSV読み込みで準備できるようにしたりなどなど改良してみてください。

この記事が面白いと思った方はぜひ、いいね、お願いします。
モチベーションにつながります。

8
0
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
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?