10
2

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 3 years have passed since last update.

ElixirAdvent Calendar 2019

Day 15

簡単な名前診断プログラムをElixirで書いてみる話(FC版ドラクエ2のサマルトリア王子編)

Last updated at Posted at 2019-12-15

この記事はElixir Advent Calendar 2019 15日目の記事です

昨日は @koga1020 さんによる「Phoenixプロジェクトをmix releaseでパッケージ化してdockerコンテナで動作させる」でした!

本日の記事は、タイトルにもあるように「FC版ドラクエ2のサマルトリア王子」の名前を診断するプログラムをElixirで書いてみます。
アドベントカレンダーなので、少しだけ遊び心を加えてみました。

概要:

本プログラムは、任意の文字列を入力して、特定のパターンの名前を出力するCLIベースのプログラムです。

特定のパターンを、「ドラゴンクエスト2」(以降、ドラクエ2)の仲間の名前作成ルールに沿って実施してみました。

ドラクエ2では、主人公「ローレシアの王子」の名前によって、仲間になる「サマルトリアの王子」「ムーンブルクの王女」の名前が自動的に決定されます。
今回は、仲間の一人「サマルトリアの王子」のみ名前を出力するようにしてみます。

なお、FC版ドラクエ2では、以下のような入力ルール/出力ルールになっています。

  • 入力ルール
  • 名前に入力できる文字は「あ〜ん」までの各文字、および「っゃゅょ゛゜」です。
  • 名前に入力できる文字数は1-4文字です。0文字、および5文字以上はエラーにします。
  • 名前に入力できる文字以外(例えば「ア」「a」など)が含まれていた場合はエラーとします。
  • 出力ルール
  • 入力された名前がエラーの場合、「ふたり」という名前を返します。
  • 入力された名前にエラーがない場合、入力された名前の使われている各文字を特定の数字に置き換えます。例えば「」なら10、「」なら11とします。
  • 各文字に割り当たった数字の合計値を算出します。
  • 合計値を64で剰余演算し、剰余の値で対応する名前を返します。なお、剰余の値によって出力する名前は以下の表の通りです。
範囲 名前
0-7 ランド
8-15 カイン
16-23 アーサー
24-31 コナン
32-39 クッキー
40-47 トンヌラ
48-55 すけさん
56-63 パウロ

なお、今回のプログラムでは、入力した名前の判定結果も返すため、プログラムの戻り値はタプルで「{判定結果, 王子の名前}」の形式にします。

名前の診断例

例えば、ローレシアの王子の名前を「ああああ」とした場合。
「あ」は10なので、10+10+10+10で合計値は40 となります。この合計値を64で剰余を求めると、そのまま40となります。
上記の表に照らし合わせると、40-47に該当する「トンヌラ」が該当します。

実行例

上記ルールをもとに、Elixirで実装してみました。
以下は、実行例です。

iex>"ああああ" |> Cannock.name_of_prince
{:ok, "トンヌラ"}

実装

Elixirのバージョン1.9.0で実装しています。
OSについてはmaxOSのMojaveで実施していますが、今回の例ではあまりOSは関係ないのでWindows環境でも実施は可能です。

モジュール名

「サマルトリア」の英語表記がわからなかったので、海外版ドラゴンクエスト2である「ドラゴンウォーリア2」のサマルトリアの地名「Cannock(カノック)」としました。

またテストもしやすいように、mixコマンドを実行して、プロジェクトを作成することにしました。

$mix new cannock
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/cannock.ex
* creating test
* creating test/test_helper.exs
* creating test/cannock_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd cannock
    mix test

Run "mix help" for more commands.

mix.exs

標準のライブラリのみ利用しているので、追記はありません。

文字数の判定

文字のリストにすることで、リストのサイズで判定することにしました。
文字列のままで判定しても良かったのですが、有効な文字かどうかを判定する際に結局文字のリストが必要になってくるので、リストで扱うようにしました。
リストのサイズを計算するのに、ヘッドとテイルに分けて加算する方法も考えたのですが、1行で書くためにlengthを利用しました。

  def valid_namesize(names \\ [] ) when is_list(names) do
    0 < length(names) && length(names) <= 4
  end

有効な文字の判定

渡された文字が1文字かどうか、その文字が文字のリストに含まれているかを判定しています。
リスト中に含まれているかどうかは、「Enum.any?/2」を利用しています。

  def valid_char(char \\ "", list \\ ["も","と","ょ"]) do
    cond do
      String.length(char) == 1 -> valid_exist_char(char,list)
      true -> false
    end
  end

  def valid_exist_char(char, list) do
    list |> Enum.any?( fn c -> c == char end)
  end

文字の変換マップ

「あ〜ん」までの各文字、および「っゃゅょ゛゜」に対して、特定の数字を割り当てます。
FC版では、以下のような数字の割り当てになっているようです。

※残念ながら、正式な変換値の出典元までは見つけられませんでした。

  def charmap() do
    map = %{"あ" => 10, "い" => 11, "う" => 12, "え" => 13, "お" => 14,
            "か" => 15, "き" => 16, "く" => 17, "け" => 18, "こ" => 19,
            "さ" => 20, "し" => 21, "す" => 22, "せ" => 23, "そ" => 24,
            "た" => 25, "ち" => 26, "つ" => 27, "て" => 28, "と" => 29,
            "な" => 30, "に" => 31, "ぬ" => 32, "ね" => 33, "の" => 34,
            "は" => 35, "ひ" => 36, "ふ" => 37, "へ" => 38, "ほ" => 39,
            "ま" => 40, "み" => 41, "む" => 42, "め" => 43, "も" => 44,
            "や" => 45, "ゆ" => 46, "よ" => 47, "ら" => 48, "り" => 49,
            "る" => 50, "れ" => 51, "ろ" => 52, "わ" => 53, "を" => 54,
            "ん" => 55, "っ" => 56, "ゃ" => 57,  "ゅ" => 58, "ょ" => 59,
            "゛" => 44, "゜" => 45 }
    map
  end

サマルトリア王子の名前候補マップ

名前の候補のマップを、前述の表の範囲に合わせて作成します。
以下を実施することで、%{0 => "ランド", 1 => "ランド", …(中略) ,7 => "ランド", 8 => "カイン", …(以降略)}のようなマップができました。

  def namemap() do
    p_name = %{0 => "ランド", 1 => "カイン", 2 => "アーサー",
               3 => "コナン", 4 => "クッキー", 5 =>"トンヌラ",
               6 => "すけさん", 7 => "パウロ"}
    p_size = p_name |> Enum.reduce(0, fn {_k,_v}, acc -> acc + 1 end)
    0..63 |> Enum.reduce(%{}, fn x, acc -> Map.put(acc, x, p_name[div(x, p_size)]) end)
  end

入力された名前の使われている各文字を特定の数字に置き換えた後で合計値を算出します

  def conv_names_to_num(name, map \\ %{"も" => 44 ,"と" => 59, "ょ" => 29}) do
    name
    |> String.codepoints()
    |> Enum.filter(fn x -> Map.has_key?(map, x)  end)
    |> Enum.map(fn x -> map[x]  end)
    |> Enum.sum
  end

合計値を利用してマルトリア王子の名前候補マップから名前を取得します


  def conv_num_to_princename(num \\ 0, map \\ %{0 => "トンヌラ"}) do
    m_size = map |> Enum.reduce(0, fn {_k,_v}, acc -> acc + 1 end)
    name = fn (n, m, s) -> m[rem(n, s)] end
    num |> name.(map, m_size)
  end

メイン処理

名前の文字列はString.codepoints()で1文字ずつのリストに変換します。
文字列のリスト化にはString.splitもありますが、String.split/1ではリストの前後に空文字が入ります。String.splitを利用する場合は、name |> String.split("", trim: true)を利用しときます。

また、バリデーション「valid」の結果がfalseになる場合、エラーの戻り値「{:ng, "ふたり"}」を返すようにしています。
validがtrueの場合は、そのままサマルトリアの王子の名前を取得します。
戻り値として、「{:ok, 取得した名前}」を返します。

  def name_of_prince(name \\ "もょもと") do
    cond do
      valid(name) == false -> {:ng,"ふたり"}
      true -> {:ok, name |> conv_names_to_num(charmap()) |> conv_num_to_princename(namemap()) }
    end
  end

  defp valid(name) do
    names = name |> String.codepoints()
    chars = charmap() |> Enum.map(fn {k,_} -> k end)
    cond do
      valid_namesize(names) == false -> false
      names |> Enum.any?(fn x -> valid_char(x, chars) == false end) -> false
      true -> true
    end
  end

実行結果

$iex -S mix
(略)

iex> "ああああ" |> Cannock.name_of_prince
{:ok, "トンヌラ"}

iex> "もょもと" |> Cannock.name_of_prince
{:ok, "すけさん"}

iex> "あわ" |> Cannock.name_of_prince
{:ok, "パウロ"}

iex> "いわ" |> Cannock.name_of_prince
{:ok, "ランド"}

iex> Cannock.name_of_prince
{:ok, "すけさん"}

iex> "" |> Cannock.name_of_prince
{:ng, "ふたり"}

iex> "あああああ" |> Cannock.name_of_prince
{:ng, "ふたり"}

iex> "あア" |> Cannock.name_of_prince
{:ng, "ふたり"}

コード

メインコードとテストコードを載せておきます。

メインコード

lib/cannock.ex
defmodule Cannock do
  @moduledoc """
  Documentation for Cannock.
  """

  @doc """
  Hello world.

  ## Examples

      iex> Cannock.hello()
      "いやー探しましたよ"

  """
  def hello do
    "いやー探しましたよ"
  end

  def valid_namesize(names \\ [] ) when is_list(names) do
    0 < length(names) && length(names) <= 4
  end

  def valid_char(char \\ "", list \\ ["も","と","ょ"]) do
    cond do
      String.length(char) == 1 -> valid_exist_char(char,list)
      true -> false
    end
  end

  def valid_exist_char(char, list) do
    list |> Enum.any?( fn c -> c == char end)
  end

  def conv_names_to_num(name, map \\ %{"も" => 44 ,"と" => 59, "ょ" => 29}) do
    name
    |> String.codepoints()
    |> Enum.filter(fn x -> Map.has_key?(map, x)  end)
    |> Enum.map(fn x -> map[x]  end)
    |> Enum.sum
  end

  def conv_num_to_princename(num \\ 0, map \\ %{0 => "トンヌラ"}) do
    m_size = map |> Enum.reduce(0, fn {_k,_v}, acc -> acc + 1 end)
    name = fn (n, m, s) -> m[rem(n, s)] end
    num |> name.(map, m_size)
  end


  def name_of_prince(name \\ "もょもと") do
    cond do
      valid(name) == false -> {:ng,"ふたり"}
      true -> {:ok, name |> conv_names_to_num(charmap()) |> conv_num_to_princename(namemap()) }
    end
  end

  defp valid(name) do
    names = name |> String.codepoints()
    chars = charmap() |> Enum.map(fn {k,_} -> k end)
    cond do
      valid_namesize(names) == false -> false
      names |> Enum.any?(fn x -> valid_char(x, chars) == false end) -> false
      true -> true
    end
  end

  def charmap() do
    map = %{"あ" => 10, "い" => 11, "う" => 12, "え" => 13, "お" => 14,
            "か" => 15, "き" => 16, "く" => 17, "け" => 18, "こ" => 19,
            "さ" => 20, "し" => 21, "す" => 22, "せ" => 23, "そ" => 24,
            "た" => 25, "ち" => 26, "つ" => 27, "て" => 28, "と" => 29,
            "な" => 30, "に" => 31, "ぬ" => 32, "ね" => 33, "の" => 34,
            "は" => 35, "ひ" => 36, "ふ" => 37, "へ" => 38, "ほ" => 39,
            "ま" => 40, "み" => 41, "む" => 42, "め" => 43, "も" => 44,
            "や" => 45, "ゆ" => 46, "よ" => 47, "ら" => 48, "り" => 49,
            "る" => 50, "れ" => 51, "ろ" => 52, "わ" => 53, "を" => 54,
            "ん" => 55, "っ" => 56, "ゃ" => 57,  "ゅ" => 58, "ょ" => 59,
            "゛" => 44, "゜" => 45 }
    map
  end

  def namemap() do
    p_name = %{0 => "ランド", 1 => "カイン", 2 => "アーサー",
               3 => "コナン", 4 => "クッキー", 5 =>"トンヌラ",
               6 => "すけさん", 7 => "パウロ"}
    p_size = p_name |> Enum.reduce(0, fn {_k,_v}, acc -> acc + 1 end)
    0..63 |> Enum.reduce(%{}, fn x, acc -> Map.put(acc, x, p_name[div(x, p_size)]) end)
  end

end

テストコード

test/cannock_test.exs
defmodule CannockTest do
  use ExUnit.Case
  doctest Cannock

  test "greets the world" do
    assert Cannock.hello() == "いやー探しましたよ"
  end

  test "valid names size" do
    assert Cannock.valid_namesize([]) == false
    assert Cannock.valid_namesize(["あ"]) == true
    assert Cannock.valid_namesize(["あ","い","う","え"]) == true
    assert Cannock.valid_namesize(["あ","い","う","え","お"]) == false
  end

  test "valid char" do
    list = ["あ","き","な"]
    assert Cannock.valid_char("あ", list) == true
    assert Cannock.valid_char("き", list) == true
    assert Cannock.valid_char("な", list) == true
    # リストの値以外はNG
    assert Cannock.valid_char("か", list) == false
    # 1文字以外はfalse
    assert Cannock.valid_char("", list) == false
    assert Cannock.valid_char("あき", list) == false
  end

  test "check char" do
    chars = Cannock.charmap() |> Enum.map(fn {k,_} -> k end)
    assert Cannock.valid_exist_char("ア", chars) == false
    assert Cannock.valid_exist_char("あ", chars) == true
    assert Cannock.valid_exist_char("゛", chars) == true
  end

  test "Convert names to numbers" do
    map = %{ "あ" => 1 , "き" => 3, "な" => 5}
    assert Cannock.conv_names_to_num("あ", map) == 1
    assert Cannock.conv_names_to_num("あき", map) == 4
    assert Cannock.conv_names_to_num("あきな", map) == 9
    assert Cannock.conv_names_to_num("", map) == 0
    assert Cannock.conv_names_to_num("すけさん", map) == 0
  end

  test "Convert num to name of cannok" do
    map = %{ 0 => "ランド" , 1 => "カイン" , 2 => "アーサー"}
    assert Cannock.conv_num_to_princename(0, map) == "ランド"
    assert Cannock.conv_num_to_princename(1, map) == "カイン"
    assert Cannock.conv_num_to_princename(2, map) == "アーサー"
    assert Cannock.conv_num_to_princename(3, map) == "ランド"
    assert Cannock.conv_num_to_princename(0) == "トンヌラ"
  end

  test "Namelist print" do
    map = Cannock.namemap()
    assert map[0] == "ランド"
    assert map[7] == "ランド"
    assert map[8] == "カイン"
    assert map[15] == "カイン"
    assert map[16] == "アーサー"
    assert map[23] == "アーサー"
    assert map[24] == "コナン"
    assert map[31] == "コナン"
    assert map[32] == "クッキー"
    assert map[39] == "クッキー"
    assert map[40] == "トンヌラ"
    assert map[47] == "トンヌラ"
    assert map[48] == "すけさん"
    assert map[55] == "すけさん"
    assert map[56] == "パウロ"
    assert map[63] == "パウロ"
  end

  test "Name of the Prince of Cannock" do
    assert Cannock.name_of_prince() == {:ok, "すけさん"}
    assert Cannock.name_of_prince("もょもと") == {:ok, "すけさん"}
    assert Cannock.name_of_prince("もょとも") == {:ok, "すけさん"}
    assert Cannock.name_of_prince("もょと゛") == {:ok, "すけさん"}
    assert Cannock.name_of_prince("くろとら") == {:ok, "アーサー"}
    assert Cannock.name_of_prince("まさひか") == {:ok, "トンヌラ"}
    assert Cannock.name_of_prince("ああああ") == {:ok, "トンヌラ"}
    assert Cannock.name_of_prince("あああああ") == {:ng, "ふたり"}
    assert Cannock.name_of_prince("アアアア") == {:ng, "ふたり"}
    assert Cannock.name_of_prince("") == {:ng, "ふたり"}
  end
end

mix testの結果

$mix test
.........

Finished in 0.2 seconds
1 doctest, 8 tests, 0 failures

まとめ

ドラクエ2の仲間の名前を診断するプログラムは、何番煎じかはわかりません。
しかし、既にあるからこそ、作るもののイメージが明確になっています。
加えて、Elixirの基礎構文の復習も兼ねて実施してみました。

また、今回はCLIプログラムで実施していますが、仕組みとしてはPhoenixを使ってWebアプリにすることも可能です。さらに、Phoenix LiveViewを利用すれば、入力と同時にサマルトリアの王子の名前をリアルタイムで画面上に出すことできです。
Webアプリ化について時間をとって、チャレンジしてみようと思います。

明日のElixir Advent Calendar 2019 は、@koduki さんによる記事となります。お楽しみに!

10
2
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
10
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?