Elixir Livebook でじゃんけんを実装する

## はじめに

Qiita の記事には「じゃんけん」の実装が結構多いようです

https://qiita.com/search?q=%E3%81%98%E3%82%83%E3%82%93%E3%81%91%E3%82%93

色々な言語で実装されています

というわけで、 Elixir で実装してみます

せっかくなので Livebook を使います

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/90_janken.livemd

## じゃんけんとは

https://ja.wikipedia.org/wiki/%E3%81%98%E3%82%83%E3%82%93%E3%81%91%E3%82%93

じゃんけんは手の形で勝敗を決する遊びです

手の形には以下の3種類があります

- ✊グー
- ✌️チョキ
- ✋パー

参加者は「じゃんけんぽん」の掛け声に合わせていずれかの手の形を提示します

グーはチョキに勝ち、チョキはパーに勝ち、パーはグーに勝ちます

同じ手の形の場合は「あいこ」になります

## セットアップ

入出力に便利な Kino をインストールしておきます

```elixir
Mix.install([
  {:kino, "~> 0.8"}
])
```

## 手と結果の定義

じゃんけんをコード化するため、以下のように定義します

### 手の形

- 0 = グー
- 1 = チョキ
- 2 = パー

```elixir
hands = [
  {0, "グー"},
  {1, "チョキ"},
  {2, "パー"}
]
```

![スクリーンショット 2022-12-19 15.14.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a829833f-a26d-881b-836c-437f6410c506.png)

`Kino.Input.select` に渡すために Tuple の List にしています

 ### 勝敗結果

- 0 = あいこ
- 1 = 勝ち
- 2 = 負け

```elixir
results = ["あいこ", "勝ち", "負け"]
```

![スクリーンショット 2022-12-19 15.14.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ef5bf788-7217-3dd4-40a4-825db20f8d02.png)

この並びには意味があります

## 手の選択

自分の手を選択できるようにします

```elixir
my_hand_input = Kino.Input.select("あなたの手", hands) 
```

![select.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/be04be6d-09da-49fe-daca-eefac4119cab.gif)

```elixir
my_hand = Kino.Input.read(my_hand_input)
```

![スクリーンショット 2022-12-19 15.18.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e82aefcd-7c88-7dc9-b6cf-ca253c339180.png)

相手の手はランダムにします

```elixir
# ランダムに決定
opponents_hand = 0..2 |> Enum.random
```

![スクリーンショット 2022-12-19 15.21.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9a4253d5-3dde-bac4-c9fb-a9f3149222f7.png)

手の名称を表示する関数を用意します

```elixir
get_hand_name = fn hand ->
  hands |> Enum.at(hand) |> elem(1)
end
```

```elixir
IO.puts("あなたの手: #{get_hand_name.(my_hand)}")
IO.puts("あいての手: #{get_hand_name.(opponents_hand)}")
```

![スクリーンショット 2022-12-19 15.31.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/7ee60eae-e452-f59e-1aa2-2858f9d8faa9.png)

## 勝敗判定

では肝となる勝敗判定を実装します

まず「あいこ」は単純で、自分と相手の数字が同じ場合です

つまり組み合わせでは

- 0:0 （グー:グー）
- 1:1 （チョキ:チョキ）
- 2:2 （パー:パー）

続いて勝ちのパターンを見てみましょう

- 0:1 （グー:チョキ）
- 1:2 （チョキ:パー）
- 2:0 （パー:グー）

最後に負けのパターンです

- 0:2 （グー:パー）
- 1:0 （チョキ:グー）
- 2:1 （パー:チョキ）

勝敗判定を関数とみなすと、以下のように考えられます

- f(0,0) = 0
- f(0,1) = 1
- f(0,2) = 2
- f(1,0) = 2
- f(1,1) = 0
- f(1,2) = 1
- f(2,0) = 1
- f(2,1) = 2
- f(2,2) = 0

これを満たす関数を定義すれば勝敗判定できそうです

勘のいい人はもう理解したと思います
（同じ解法はおそらくすでに世にあるものと思います）

相手の手 - 自分の手は以下のようになります

- d(0,0) = 0
- d(0,1) = 1
- d(0,2) = 2
- d(1,0) = -1
- d(1,1) = 0
- d(1,2) = 1
- d(2,0) = -2
- d(2,1) = -1
- d(2,2) = 0

これに 3 を足してみます

- d'(0,0) = 3
- d'(0,1) = 4
- d'(0,2) = 5
- d'(1,0) = 2
- d'(1,1) = 3
- d'(1,2) = 4
- d'(2,0) = 1
- d'(2,1) = 2
- d'(2,2) = 0

3 で割った余りを計算します

- d''(0,0) = 0
- d''(0,1) = 1
- d''(0,2) = 2
- d''(1,0) = 2
- d''(1,1) = 0
- d''(1,2) = 1
- d''(2,0) = 1
- d''(2,1) = 2
- d''(2,2) = 0

この d''（） 関数は f() 関数と同じですね

というわけで、じゃんけんの勝敗判定は以下の式で表せます

```elixir
fn x_hand, y_hand ->
  rem(y_hand - x_hand, 3)
end
```

手の名称表示、結果の名称表示も入れて、以下のような関数を定義しましょう

```elixir
judge = fn x_hand, y_hand ->
  IO.puts("あなたの手: #{get_hand_name.(x_hand)}")
  IO.puts("あいての手: #{get_hand_name.(y_hand)}")
  result = rem(y_hand - x_hand, 3)
  result_name = Enum.at(results, result)
  IO.puts("結果: #{result_name}")
  result_name
end
```

呼び出してみます

```elixir
judge.(my_hand, opponents_hand)
```

![スクリーンショット 2022-12-19 15.48.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6184d859-7780-46be-c438-ba1fbb86848a.png)

## 全ての組み合わせでテスト

では全ての組み合わせを作ってテストします

```elixir
%{
  "あいての手\\あなたの手" => Enum.map(hands, &elem(&1, 1))
}
|> Map.merge(
  for {x_hand, x_hand_name} <- hands, into: %{} do
    {
      x_hand_name,
      Enum.map(hands, fn {y_hand, _} ->
        judge.(x_hand, y_hand)
      end)
    }
  end
)
|> Kino.DataTable.new()
```

![スクリーンショット 2022-12-19 15.50.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/39ad3a08-ca85-e4be-86c2-8f10b5fde148.png)

どの組み合わせでも正しい結果になっていますね

## まとめ

こういうのは気分転換できていいですね