この記事は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, "ふたり"}
コード
メインコードとテストコードを載せておきます。
メインコード
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
テストコード
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 さんによる記事となります。お楽しみに!