動けば良いの！？要件定義ミスと揃わないビンゴ

こんにちは！去年は[MIDIキーボードの記事](https://qiita.com/Lundy/items/2c568f17b9df9b9602e0)を書いてました、$Lundy$ です！
一年ぶりの N/S高等学校 Advent Calendar、様々な技術的な記事に圧倒されてそういった記事を書くのは恐れ多いので、
今年は、プログラミングの中でもあまり注目されない**ある作業**をテーマに、体験談ベースの話を書かせていただきます！

この記事は二部構成です。

第一章では、今回の体験談を紹介しています。
プログラミングに関して知識が無くても読めるように工夫したので、専門外の方もぜひ！

第二章では、開発過程を紹介しています。
ビンゴの生成にまつわるオリジナルのアルゴリズムを開発したので、紙とペンで手元でも再現できる手順を紹介しています。こちらもぜひ！

### テーマは「要件定義」
あまり注目されない**ある作業**こそ、今回のテーマ「要件定義」です。
要件定義とは、開発を始める前に、何を作れば良いかを明確にする作業のこと。
この記事では「要件定義」のミスを巡る失敗について体験談ベースで書いています。時系列に沿ってエピソードを紹介するので、ぜひ要件定義のミスを探しながら読んでみてください！

# 第一章 オリジナルビンゴカードの開発
時は2023年10月、僕が通う代々木キャンパスでは「キャンパスフェスティバル」という文化祭のようなものの予定が組まれていました。僕は実行委員じゃ無かったので詳しいことは知りませんが、ステージを始め、様々な出し物が行われる予定だったとのこと。
そんな中、ステージで開催されるコスプレ大会で、オリジナルルールのビンゴを行うという企画が立ったそうです。
ルールに沿ったオリジナルのビンゴカードを作りたいが、大量生産を手動で行うのは手間で厳しい。
そこで「ビンゴカードの自動生産システムを作ってほしい」と依頼が来ました。

## 企画内容
これは初めに僕が受けた説明です。

### コスプレ大会
- ステージでコスプレ大会を行う。
- 出演者は各々好きなコスプレをして舞台に上がり、２分程度の紹介文を読み上げる。

### オリジナルビンゴゲーム
- コスプレ大会に付け加える形でビンゴ大会を実施する。
- 参加者には「出演者の紹介文に出現する単語をマスに埋め込んだオリジナルビンゴカード」が配られ、自分のカードにある単語がステージで読み上げられたら、対応する単語に穴を開けることができる。

## 要件定義
続いて、この説明を元にさくさく話し合い、完成した要件定義です。

### オリジナルビンゴカードの開発

- 提供される単語リストを元に、5x5のマスにランダムに単語が配置されたビンゴカードを作成する。
- 必要な枚数は未定なので、配置に被りのないカードを無限に生成できるシステムを開発する。
- 中心はFREE SPACEのマスにする。
- 最終的には印刷するものとして、カードの画像データを用意して納品する。


さて、これで開発はうまくいくのでしょうか。

## 開発
開発言語はPython3、画像処理を行うライブラリPillowなどを活用して開発しました。

Pillowが日本語対応してなかったりでいろいろ詰みましたが、２時間程度でなんとか完成。
早速実行委員に納品します。

〜この先ネタバレ〜

## 納品と衝撃
完成品は好評でした。初めに定義した要件は全て満たしていて、ランダムな配置のカードが半永久的に生成され続けるシステムはまさにロマン。
しかし、

ある一人が一言。
「これって、どのカードも全部揃うくない？」

ビンゴゲームをするのに、全員のカードが揃ってしまうという致命的な欠陥に気づいたのでした。

プログラムは間違っていないのに、根本的な不具合。
これこそまさに、「要件定義」のミスです。

## どうすればよかったのか

今回の最大のミスは、確認を怠った要件定義にありました。
全員のフィーリングだけで、実際の運用も想定せずに作り上げた要件定義。今回は運が悪く大きな穴ができてしまっていたようです。

要件通りにシステムを作るとどうなるかを想像して、確認することはできたはずでした。
確認さえしていればこんなことにはならなかったのに・・・（コードがガラガラ崩れる音）

それでは、まとめに入らせてください。

# 要件定義は運用を想定して！！！！！
これが言いたい！！！！要件定義っていう漢字を見直してください！！！
適当に決めた要件ではいつか破綻します！しっかりと運用を想定して、何が必要なのかを明確に定義してください！！！

要件が正確に設定されて初めて要件定義なのです。
プログラマーの仕事はプログラムを書くことで、設計ではありません。
より正確な要件定義を行うことで、設計と開発を完全に分離でき、より安定したストレスフリーな開発を行うことができるのではないでしょうか。

って、今回の失敗で感じました。

# 第二章 ビンゴカード開発RTA
では、ここからは開発過程をご紹介します。
特に要件定義のミスが発覚した後、一からコードを作り直した悲しい過程をご紹介。
（納期が"""１日"""だったので、動けばヨシ！としてRTAのように実装したコードがあります。すみません。）

### 開発言語
Python3

## 機能を分解
今回開発した機能を大きく分解すると、
- 文字を書き込んだ画像を生成する機能（要日本語対応）
- ランダムな単語の配置を作るアルゴリズム

の２つ。これらをそれぞれ紹介します。

## 文字を書き込んだ画像を生成する機能
画像処理ライブラリPillowを使って、画像に文字を書き込む機能を実装します。
実はここがかなり大変でした。Pillowって日本語対応してないんですね。
とはいえ方法が無いわけではなく、以下の記事で大体実装できました。

https://qiita.com/Kazuhito/items/6a030b3746d941c3d9c7

他の大部分はRTAで生まれた冗長コードなのでカット。

## ランダムな単語の配置を作るアルゴリズム
今回特に紹介したいのがこちらです。

先ほどの体験談で、欠点に気付いた後の話。依頼側から、
「ビンゴが揃う人数を把握したい」
との旨が。これって言い換えると、
**「絶対に揃うビンゴ」と「絶対に揃わないビンゴ」を作ってほしい**
となりますよね。
始めの仕様ではランダムに単語を配置するだけだったのでただシャッフルしておわりでしたが、この要件に対応するために一から作り直しとなりました。

調べても前例はナシ。自作のアルゴリズムを作ることになりました。
そんなこんなで完成した、
絶対に揃わないビンゴを作るアルゴリズムがこちらです。↓

## 絶対に揃わないビンゴを作るアルゴリズム
紙とペンがあれば手元でも簡単に作ることが出来ます！試してみてね。
○を「穴が開くマス」、空白を「穴が開かないマス」として表記します。

### 手順0
まず5x5マスの土台となるマスを準備します。

<img width="250" alt="step0.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/234f2bec-6010-0d8d-d760-41f1b0d6d04e.png">

### 手順1
2~4行目に注目して、
1行に４つずつ、○をランダムに配置します。

<img width="250" alt="step1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/1c8ee858-7c60-b302-3f6b-09794432f404.png">

### 手順2
2~4列目の各列に注目して、
- もし真ん中の**縦３マスが全て○**で埋まっていたら、
上下どちらか１マスに○を配置。
- 真ん中の**縦３マスのうち、○が２個以下**なら
上下両方に○を配置します。
<img width="250" alt="step2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/da9ca9bc-449f-2dc0-f2a0-93da5f1d34ed.png">

### 手順3
四隅のマスに注目して、
一つ一つビンゴにならないかを確認しながら埋めていきます。
- 左上
どこにもリーチがなく、ビンゴは揃わないので○を配置。
<img width="250" alt="step3-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/b5972542-a3e9-8a87-e5dd-fcaf57bb992d.png">

- 右上
同様にビンゴは揃わないので○を配置。
<img width="250" alt="step3-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/ec547c48-b60f-8e66-6b2c-25e5e6c10b5b.png">

- 左下
この場合は列でビンゴしてしまうため、スキップ。
<img width="250" alt="step3-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/ed9cac3d-f93d-68d6-c9d7-2b01ed24371b.png">

- 右下
同様に列でビンゴしてしまうため、スキップ。
<img width="250" alt="step3-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/15f17837-a018-abbc-67a9-4b3ec9cb8473.png">

### 完成！
以上の手順を踏むことで、絶対に揃わないビンゴカードを作ることができます！
今回はこのようなビンゴが出来ました。
この部分だけが最終的に開くようなビンゴゲームを開催すれば、絶対に揃わないビンゴゲームの完成です。

<img width="250" alt="step3-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/63adc4c3-df0b-efd9-030e-5342773aa830.png">

ちなみに手順3を無視して四隅を埋めることで「絶対に揃うビンゴ」を作ることが出来ます。
真ん中の9マスに注目すると、
手順1で○が6~9個入り、ど真ん中が仕様上FREE SPACE（○）にになるので、四隅を埋めることで必ず斜めで一つビンゴします。

ちなみに実際に配布したビンゴカードはこんな感じ。

<img width="300" alt="step3-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3073290/afb23ac3-6276-406c-4804-d632efb7a17b.png">


# まとめ
ということで、最終的にはこのようなアルゴリズムを開発し、なんとか実装、再度納品することが出来ました。納期もセーフ。

なにより一番楽しかったのは、何も知らない参加者のビンゴカードを覗き、「あ、これ揃わないやつだ」とか勝手に考えてたことです。黒幕のような気分。

なお、今回のアルゴリズムを実際に実装したPythonのソースコードを記事の一番下に添付しています。読める方はご参照ください。

最後に、繰り返しにはなりますが、一度で開発が終わるのが理想です。
要件定義にはご注意を。

# アルゴリズムをPythonで実装
```python
import random

def generate_bingo_set(unmatched = False) -> list[list]:
  l = [
    [0,0,0,0,0],
    [0,0,0,0,0],
    [0,0,1,0,0],
    [0,0,0,0,0],
    [0,0,0,0,0]
  ]

  ### 1.横向きに埋める
  for i in range(1,3+1):
    l[i] = random.sample([0,1,1,1,1], 5)

    # FREESPACEを考慮する
    if i == 2 and l[i][2] == 0:
      l[i][2] = 1
      l[i][random.choice([0,1,3,4])] = 0


  ### 2.縦向きに埋める
  for i in range(1,3+1):
    s = [l[1][i],l[2][i],l[3][i]]

    if s == [1,1,1]:
      l[random.choice([0,4])][i] = 1
    else:
      l[0][i] = 1
      l[4][i] = 1


  ### 3.ナナメを確認して埋める
  def get_samples(s:list) -> list[list]:
    return [
      s[0],
      s[4],
      [s[0][0],s[1][0],s[2][0],s[3][0],s[4][0]],
      [s[0][4],s[1][4],s[2][4],s[3][4],s[4][4]],
      [s[0][0],s[1][1],s[2][2],s[3][3],s[4][4]],
      [s[0][4],s[1][3],s[2][2],s[3][1],s[4][0]]
    ]

  # 揃うか揃わないか
  if unmatched:
    # 左上 は絶対に揃わない
    l[0][0] = 1

    # 右上
    row_0, *_ = get_samples(l)

    if row_0[:-1] != [1,1,1,1]:
      l[0][4] = 1

    # 左下
    _, _, col_0, _, _, diag_4 = get_samples(l)

    if col_0[:-1] != [1,1,1,1] and diag_4[:-1] != [1,1,1,1]:
      l[4][0] = 1

    # 右下
    _, row_4, _, col_4, diag_0, _ = get_samples(l)

    if [1,1,1,1] not in [row_4[:-1], col_4[:-1], diag_0[:-1]]:
      l[4][4] = 1

  else:
    # 四隅を空けると絶対に揃う
    l[0][0] = 1
    l[0][4] = 1
    l[4][0] = 1
    l[4][4] = 1

  return l
```

# あとがき
### 今年もアドベントカレンダー
N/S高等学校 Advent Calendarには2年連続の参加が叶って嬉しいです。
今年も様々な方の尖った面白い記事があるので、ぜひとも他の方の記事も覗いてみてください！

https://qiita.com/advent-calendar/2023/n-s-highschool