ビンゴカードを生成してくれるAPIをつくる

こんにちは。

複数プレイヤーが参加可能なビンゴゲームサイトを作成しようとしている者です。

期限は特にないし、完全に趣味でつくるだけなので、気が向いたときに進めていく案件です。

ひとまず、少しずつ機能をつくっていくために、今回はビンゴカードを生成してくれるAPIを、Cloudflare Workers環境でつくりたいと思います。

:::note
最終的には、フロントエンドをVue.jsでつくって、Cloudflare Pagesにデプロイし、サーバーサイドをTypeScriptでつくって、Cloudflare Workersにデプロイしようとしています。
:::

# ベース

Hello Worldしてくれる状態からスタートします。

```ts
export default {
	async fetch(request, env, ctx): Promise<Response> {
		return new Response('Hello World!');
	},
} satisfies ExportedHandler<Env>;
```

# APIの入り口

```ts
export default {
	async fetch(request, env, ctx): Promise<Response> {
		if (request.method === 'POST') {
			if (request.url.endsWith('/v1/cards')) {
				return new Response('Hello Bingo Player!');
			}
		}
		return new Response('Hello World!');
	}
} satisfies ExportedHandler<Env>;
```

`POST POST http://127.0.0.1:8787/v1/cards`でアクセスすることにします。

POSTリクエストのため、Webブラウザの一般的なアクセスでは取得できないため、今後も見越して、VSCodeへ「REST Client」拡張機能をインストールします。

そして、`request.http`というファイルを準備し、以下のような記述をします。

```
POST http://127.0.0.1:8787/v1/cards
```

これで、上記の記載の上に「Send Request」実行ボタンが現れるような気がするので、これをクリックすると、リクエストが送信されます。

ひとつのファイルに複数のリクエストを記載して、必要なときに必要なリクエストをひとつ実行する場合は、`###`行でリクエストを区切ります。

さて、これで以下のようなレスポンスを取得できました。
OKです。

```
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/plain;charset=UTF-8
Content-Encoding: gzip

Hello Bingo Player!
```

# BingoCardクラス作成

コードがとっちらかるのを防ぐため、適宜クラスを導入していく方針とします。

さしあたって、ビンゴカードの生成およびビンゴの判定をしてくれる予定のBingoCardクラスを作成します。

```ts
class BingoCard {
	card: number[][];

	constructor() {
		this.card = this.generatedBingoCard();
	}

	private generatedBingoCard(): number[][] {
		return [];
	}

	getCard(): number[][] {
		return this.card;
	}
}

export default {
	async fetch(request, env, ctx): Promise<Response> {
		if (request.method === 'POST') {
			if (request.url.endsWith('/v1/cards')) {
				const card = new BingoCard();
				return new Response(
					JSON.stringify({ card: card.getCard() }),
					{ status: 200, headers: { 'Content-Type': 'application/json' } }
				);
			}
		}
		return new Response('Hello World!');
	}
} satisfies ExportedHandler<Env>;
```

これで、空のビンゴカード配列をレスポンスしてくれるようになりました。

```
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json
Content-Encoding: gzip

{
  "card": []
}
```

# ビンゴカード生成

今回の要ともいえる、ビンゴカードの生成です。

今回下調べして知ったのですが、ビンゴカードの数字の配列は単純なランダムではなく、第一列は1～15、第二列は16～30、第三列は31～45までの数字から五つ選ばれるらしいです。（ただし、第三列は中心がフリーであるため、四つ）

（手作業で数字を探す上で、番号を見つけやすくするための措置なのかな？）

今回はこれに従います。

基本の第一列、1～15の数字の列は、以下の規則で作成します。

1. [1, 2, 3, ..., 14, 15]の配列を作成
1. 配列内の要素をシャッフル
1. 配列の先頭5要素を取り出す

## 連番の配列を作成

```ts
const line: number[] = [...Array(15)].map((_, i) => i + 1);
```

こちらを参考にさせていただきました。

https://qiita.com/suin/items/1b39ce57dd660f12f34b

**勉強項目**

- Array(15)

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/Array#arraylength

- ...(Spread operator)

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax

## 配列内の要素のシャッフル

```ts
private shuffleArray(array: number[]): number[] {
    for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
}
 ```

 基本的な戦略は、要素の交換です。

 調べた限り、シャッフル手法としてはこの「フィッシャー–イェーツのシャッフル」Fisher–Yates shuffleと呼ばれるものの改良版が一般的らしいです。
 
 配列の末尾の要素から着目し、末尾の要素を前の要素のいずれかと交換して（あるいは交換せずに）、末尾から確定していくもの――と理解しました。

`Math.floor(Math.random() * (i + 1))`は一見ややこしいですし、実際、未だに頭に入ってきていませんが、シンプルにいえば`0～i`までの整数をランダムに生成する記述らしいです。

1. `Math.random()`で0以上1未満の小数を生成
1. 小数に`(i + 1)`をかけて、0以上i + 1未満の小数へ変換
1. 小数の小数点以下を切り捨て、0～iまでの整数へ変換

（すんなり理解できずにいるのは、この内の二項目目です。わかりそうでわからない。たとえば0.9999をかけると、i+1はi+1より小さくなるんだなあ、とか思うんですけどね。でも、未だに納得できずにいる）

ひとまず、この戦略で、配列をシャッフルします。

## 配列の先頭5要素を取り出す

```ts
shuffledLine.slice(0, 5);
```

ここはふつうにスライスするだけです。

## 拡張

基本の処理を書いたら、これを第五列まで適用できるように一般化します。

1～15を16～30や31～45に置き換えるだけなので、話は単純で、最初の配列の生成に15をいい感じに加算するだけです。

```ts
for (let n = 0; n < 5; n++) {
    const line: number[] = [...Array(15)].map((_, i) => i + 1 + n * 15);
    const shuffledLine = this.shuffleArray(line);
    card.push(shuffledLine.slice(0, 5));
}
```

配列のシャッフルと5要素の取り出しは、手を加える必要がありません。

## まとめ

クラス全体は以下のようになりました。

```ts
class BingoCard {
	card: number[][];

	constructor() {
		this.card = this.generatedBingoCard();
	}

	private generatedBingoCard(): number[][] {
		let card: number[][] = [];
		for (let n = 0; n < 5; n++) {
			const line: number[] = [...Array(15)].map((_, i) => i + 1 + n * 15);
			const shuffledLine = this.shuffleArray(line);
			card.push(shuffledLine.slice(0, 5));
		}
		return card;
	}

	private shuffleArray(array: number[]): number[] {
		for (let i = array.length - 1; i > 0; i--) {
			const j = Math.floor(Math.random() * (i + 1));
			[array[i], array[j]] = [array[j], array[i]];
		}
		return array;
	}

	getCard(): number[][] {
		return this.card;
	}
}
```

# 動作確認

これで、`POST http://127.0.0.1:8787/v1/cards`リクエストで以下が取得できるようになりました。

```
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json
Content-Encoding: gzip

{
  "card": [
    [ 1, 13, 5, 2, 4 ],
    [ 17, 21, 20, 16, 27 ],
    [ 43, 44, 38, 33, 41 ],
    [ 47, 58, 55, 48, 56 ],
    [ 74, 66, 70, 63, 65 ]
  ]
}
```

一般的なビンゴカードとは行列が反対ですが、これは（忘れていなければ）フロントエンドでひっくり返しましょう。

# 終わりに

ビンゴカードを生成してくれるAPIが完成しました。

いつの日かビンゴゲーム大会が開催できるよう、今後も少しずつ機能を実装していくことにしましょう。

ここまで、読んでいただきありがとうございました。