Skip to content

3.タイピングゲームの機能をElixir風に改良する

Suzuki-Takumi0505 edited this page Jun 2, 2022 · 1 revision

演習3-1

お題をElixirの関数にします。

  1. Typing.Editor.GameEditor構造体のchar_listに以下の表の要素を持ったリストを割り当ててください。
お題
"Enum.map([1, 2, 3], fn a -> a * 2 end)"
"Enum.shuffle([1, 2, 3])"
"Enum.reverse([1, 2, 3])"
"Map.put(%{a: "a", b: "b", c: "c"}, :d, "b")"
"Enum.map([1, 2, 3], fn a -> a * 2 end)

ヒント

  • 現在スペースも入力を受け付けないようにしているのでガード句の条件からスペースを外します。

【回答】演習3-1

  • editor/game_editor.ex
def construct() do
  char_list =
    [
      "Enum.map([1, 2, 3], fn a -> a * 2 end)",
      "Enum.shuffle([1, 2, 3])",
      "Enum.reverse([1, 2, 3])",
      "Map.put(%{a: \"a\", b: \"b\", c: \"c\"}, :d, \"b\")",
      "Enum.map([1, 2, 3], fn a -> a * 2 end) |> Enum.shuffle()"
    ]

  display_char = hd(char_list)

  %__MODULE__{
    display_char: display_char,
    char_count: String.length(display_char),
    game_status: 1,
    char_list: char_list
  }
end

ガード句の条件からスペースを外します。

  • eidtor/game_editor.ex
@exclusion_key ~w(
  Tab
  Control
  Shift
  CapsLock
  Alt
  Meta
  Eisu
  KanjiMode
  Backspace
  Enter
  Escape
  ArrowLeft
  ArrowRight
  ArrowUp
  ArrowDown
)

演習3-2

入力した関数を実行させて結果を表示させるように以下を実装してください。
※今回はお題の関数を.exsに書き込みそのファイルを実行させます。

  1. priv/staticディレクトリにresult.exsを作成してください。
  2. update/3 で記述している next_char(editor, key) はコメントアウトまたは削除してください。
  3. Typing.Editor.GameEditor構造体に以下の表のキーを追加してください。
  4. 全ての入力し終わったらresult.exsファイルに関数を書き込み、実行させてください。
  5. main.html.heex にresultの値を表示させてください。(<h2>タグで囲みます)
キー 値の型
result any

ヒント
Code.eval_file/2
File.write/3
ビュートテンプレート

  • File.write/3を使用してお題の関数をresult.exsに書き込みます。
  • Code.eval_file/2の第1引数にresult.exsのパスを渡します。
  • Code.eval_file/2で実行した値をTyping.Editor.GameEditor構造体のresultに割り当てます。
  • テンプレートでリストなどの値を表示させるにはinspectを使用します。

【回答】演習3-2

Typing.Editor.GameEditor構造体にresultを追加します。

  • editor/game_editor.ex
defstruct input_char: "",
          display_char: "",
          char_count: 0,
          now_char_count: 0,
          failure_count: 0,
          game_status: 0,
          char_list: [],
          clear_count: 0,
          result: nil

お題の関数をファイルに書き込み実行させるモジュールと関数を作成します。
utilsディレクトリにexecution.exを作成します。

  • utils/execution.ex
defmodule Typing.Utils.Execution do
  @file_path("priv/static/result.exs")

  def execution(expr) do
    File.write(@file_path, expr)

    Code.eval_file(@file_path)
  end
end

Typing.Editor.GameEditorの関数でTyping.Utils.Executionモジュールを使用できるようにエイリアスを指定します。

  • editor/game_editor.ex
defmodule Typing.Editor.GameEditor do
  import Typing.Utils.KeysDecision
  alias Typing.Utils.Execution

game_editorにdisplay_charの値をTyping.Utils.Execution.execution/1に渡す関数を作成します。

  • editor/game_editor.ex
defp display_result(editor, key) do
  {result, _} = Execution.execution(editor.display_char)

  %{
    editor
    | result: result,
      input_char: editor.input_char <> key,
      clear_count: editor.clear_count + 1
  }
end

update/3 で display_result/2 を呼び出すように記述します。

  • editor/game_editor.ex
def update(%__MODULE__{display_char: char, now_char_count: count} = editor, "input_key", %{"key" => key})
    when key not in @exclusion_key and key_check(char, count, key) and editor.game_status == 1 do
  cond do
    editor.now_char_count == editor.char_count - 1 ->
      display_result(editor, key)

    true ->
      %{editor | input_char: editor.input_char <> key, now_char_count: editor.now_char_count + 1}
  end
end

テンプレートでresultを表示させるようにします。

  • game_editor/main.html.heex
<h2>
  <%= if @editor.game_status == 1 do %>
    <span style="color: blue;"><%= @editor.input_char %></span><%= trem_display_char(@editor) %>
  <% else %>
    <%= @editor.display_char %>
  <% end %>
</h2>

<p>クリアした回数:<%= @editor.clear_count %> 回</p>
<p>ミスした回数:<%= @editor.failure_count %> 回</p>

<h2>実行結果</h2>
<h2><%= if @editor.result, do: inspect(@editor.result), else: "" %></h2>

<div 
  phx-window-blur="page-inacive"
  phx-window-focus="page-active"
  phx-window-keyup="toggle_input_key">
</div>

演習3-3

入力して実行結果を表示させた時に Enter を押して、次のお題に進めるように実装します。

  1. game_status の状態に2(Enter入力待ち)を追加することにします。
  2. テンプレートでEnterを押してくださいを表示させてください。(<h1>タグで囲みます)

ヒント

  • update/3 に Enter と game_statusが2の状態を受け付けられるようにガード句付きで追加します。

【回答】演習3-3

Enterキーとgame_statusが2の状態を受け付けるように update/3 を追加します。

  • editor/game_editor.ex
def update(%__MODULE__{} = editor, "input_key", %{"key" => key})
    when key == "Enter" and editor.game_status == 2 do
  next_char(editor, key)
end

game_statusに状態が追加されたのでnext_char/2とdisplay_result/2を変更します。

  • editor/game_editor.ex
defp next_char(editor, key) do
  char_list = List.delete(editor.char_list, editor.display_char)

  case length(char_list) do
    0 ->
      %{
        editor
        | char_list: char_list,
          display_char: "クリア",
          input_char: editor.input_char <> key,
          game_status: 0,
          result: nil
      }

    _num ->
      display_char = hd(char_list)

      %{
        editor
        | char_list: char_list,
          display_char: display_char,
          input_char: "",
          char_count: String.length(display_char),
          now_char_count: 0,
          game_status: 1,
          result: nil
      }
  end
end


defp display_result(editor, key) do
  {result, _} = Execution.execution(editor.display_char)

  %{
    editor
    | result: result,
      game_status: 2,
      input_char: editor.input_char <> key,
      clear_count: editor.clear_count + 1
  }
end

テプンレートにEnterを押してくださいと表示させるようにします。

  • editor/main.html.heex
<h2>
  <%= if @editor.game_status == 1 do %>
    <span style="color: blue;"><%= @editor.input_char %></span><%= trem_display_char(@editor) %>
  <% else %>
    <%= @editor.display_char %>
  <% end %>
</h2>

<p>クリアした回数:<%= @editor.clear_count %> 回</p>
<p>ミスした回数:<%= @editor.failure_count %> 回</p>

<h2>実行結果</h2>
<h2><%= if @editor.result, do: inspect(@editor.result), else: "" %></h2>

<%= if @editor.game_status == 2 do %>
  <h1>Enterを押してください</h1>
<% end %>

<div 
  phx-window-blur="page-inacive"
  phx-window-focus="page-active"
  phx-window-keyup="toggle_input_key">
</div>

演習3-4

現在以下のようにエラーになる関数を実行しようとするとエラーが発生して先に再マウントされます。

[error] GenServer #PID<0.621.0> terminating
** (UndefinedFunctionError) function Enum.map/1 is undefined or private
    (elixir 1.13.3) Enum.map([1, 2, 3])
    (stdlib 3.17.1) erl_eval.erl:685: :erl_eval.do_apply/6
    (elixir 1.13.3) src/elixir.erl:296: :elixir.recur_eval/3
    (elixir 1.13.3) src/elixir.erl:274: :elixir.eval_forms/3
    (elixir 1.13.3) lib/code.ex:404: Code.validated_eval_string/3
    (typing 0.1.0) lib/typing/editor/game_editor.ex:120: Typing.Editor.GameEditor.display_result/2
    (phoenix_live_view 0.17.9) lib/phoenix_live_view.ex:887: Phoenix.LiveView.update/3
    (typing 0.1.0) lib/typing_web/live/game_editor_live.ex:19: TypingWeb.GameEditorLive.handle_event/3
    (phoenix_live_view 0.17.9) lib/phoenix_live_view/channel.ex:382: anonymous fn/3 in Phoenix.LiveView.Channel.view_handle_event/3
    (telemetry 1.1.0) /apps/typing/deps/telemetry/src/telemetry.erl:320: :telemetry.span/3
    (phoenix_live_view 0.17.9) lib/phoenix_live_view/channel.ex:215: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 3.17.1) gen_server.erl:695: :gen_server.try_dispatch/4
    (stdlib 3.17.1) gen_server.erl:771: :gen_server.handle_msg/6
    (stdlib 3.17.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: %Phoenix.Socket.Message{event: "event", join_ref: "4", payload: %{"event" => "toggle_input_key", "type" => "keyup", "value" => %{"key" => ")"}}, ref: "26", topic: "lv:phx-FvDAnHdZpI3YlQSi"}
State: %{components: {%{}, %{}, 1}, join_ref: "4", serializer: Phoenix.Socket.V2.JSONSerializer, socket: #Phoenix.LiveView.Socket<assigns: %{__changed__: %{}, editor: %Typing.Editor.GameEditor{char_count: 19, char_list: ["Enum.map([1, 2, 3])", "Enum.map([1, 2, 3], fn a -> a * 2 end)", "Enum.shuffle([1, 2, 3])", "Enum.reverse([1, 2, 3])", "Map.put(%{a: \"a\", b: \"b\", c: \"c\"}, :d, \"b\")", "Enum.map([1, 2, 3], fn a -> a * 2 end) |> Enum.shuffle()"], clear_count: 0, display_char: "Enum.map([1, 2, 3])", failure_count: 0, game_status: 1, input_char: "Enum.map([1, 2, 3]", now_char_count: 18, result: nil}, flash: %{}, live_action: :index, page_title: "タイピングゲーム", template: "main.html"}, endpoint: TypingWeb.Endpoint, id: "phx-FvDAnHdZpI3YlQSi", parent_pid: nil, root_pid: #PID<0.621.0>, router: TypingWeb.Router, transport_pid: #PID<0.615.0>, view: TypingWeb.GameEditorLive, ...>, topic: "lv:phx-FvDAnHdZpI3YlQSi", upload_names: %{}, upload_pids: %{}}

このようなエラーになる関数を入力した際に画面にエラーの種類を表示させるように実装してください。

  1. Typing.Editor.GameEditor構造体のchar_list に割り当てる値(リストの先頭)に以下を追加してください。
  2. 入力した値を実行してエラーが出た際にそのエラーの種類を出力できるよにしてください。

char_listのリストに追加する値

"Enum.map([1, 2, 3])",
"Enum.map(1, fn a -> a end)",
"String.split(a, \" \")",

ヒント
try/1

  • try/1 を使用して発生したエラーをスローします。
  • スローしたエラーをパターンマッチを使ってエラーのモジュールを取得しその値を返します。
  • エラーが発生しなかった場合はそのまま、その値を返します。
  • case を使いパターンマッチをさせて値を取得します。

【回答】演習3-4

エラーになる関数を渡された際にエラーの種類を渡せるようにします。

  • utils/execution.ex
def execution(expr) do
  File.write(@file_path, expr)

  try do
    throw(Code.eval_file(@file_path))
  rescue
    x ->
      %module{} = x
      module = String.replace("#{module}", "Elixir.", "")
      "#{module}"
  catch
    x -> x
  end
end

GameEditorで戻ってくる値の形が違うのでcase のパターンマッチを使って値を取得します。

  • editor/game_editor.ex
defp display_result(editor, key) do
  result =
    case Execution.execution(editor.display_char) do
      {r, _} -> r

      error -> error
    end

  %{
    editor
    | result: result,
      game_status: 2,
      input_char: editor.input_char <> key,
      clear_count: editor.clear_count + 1
  }
end