JS + GAS + AWSでルーレットゲームを作ってウェディングパーティの余興を担当する（ソース付き）

# はじめに

![おたのしみスロット](https://qiita-image-store.s3.amazonaws.com/0/106259/9b1ca1ee-da87-aa6d-c7db-82b5a2639ffd.png)

友人👰がこの度めでたく結婚することとなり、友人・知人向けにパーティが開かれたのですが、その式の余興のひとつとしてルーレットゲームの制作を依頼され、開発と本番運用（言い方）を担当しました

他人と仕様を詰めて個人開発したWebアプリとしては初めてのものになるので、色々至ってない部分も多いですが、そうした諸々の備忘も兼ねて書き残したいと思います🎰

# 受注

今年の梅雨の頃に結婚することを呑みの席で切り出された時、私はちょうど退職に向けて準備を進めていたところで、夏の間に数ヶ月の休みを取るつもりでいました

その間を無為に過ごすことを味気なく思い、「しばらく暇だしなんなら余興でも作ったげるよ😎」と軽い気持ちで言いおいてその場は終わったのですが、後日連絡をもらい、冬に開かれるパーティに向けてルーレットのゲームを作ってほしい、と改めて依頼を受けました

前職ではWebアプリを作っていたことと、当日まで半年近く期間があったため、まあなんとかなるだろうと軽い気持ちで承諾し、プロジェクトがスタートしました

# 要件定義

ファミレスで2時間ほど彼女と話したところ、おおよそ以下のようなことを言われました

- ウェディングパーティに来てくれた人に、ルーレットゲームで景品をプレゼントしたい
	- 色々なグループ（地元の友達、会社の同僚、社会人サークルの方々...など）から、あわせて70~80人くらいの人を招待するつもり
- 景品には当たり（布団乾燥機とか...）とハズレ（愛用のつけまつげとか...）をそれぞれいくつか用意する
- 招待状（[Biluce](https://biluce.net/)というWebサービスを使ってました）の返信をもらう際に参加者に画像を添付してもらい、ルーレットで使いたい
- 景品も画像で用意したい
- 当日のルーレット操作は私（筆者）にやってほしい
- 当日ドタキャンや人数増加などが発生する可能性があるので、参加者の増減はゲーム直前まで柔軟に対応できるようにしてほしい

そんなところだろうという感じですが、面白かったのが以下でした

- ルーレットの出目を制御したい
	- ハズレの景品を社会人サークルの重鎮の方や大人しい友達に当てると気まずいので、ある景品を特定の所属の人や個人には当てないようにしたい
	- グループごとに参加人数に差があるが、景品の当たる回数はグループ間でなるべく均一にしたい

要は、忖度したいということですね。気遣いのできる良妻となることでしょう（適当）

# 設計

家に帰って、早速アーキテクチャを考えました。事前に新婦に参加者および景品の登録や、アプリの動作確認をしてもらうことを考えると、Webアプリとして実装してクラウドサービスで公開するのがよさそうです⛅️

また、データの参照、更新が複数人から頻繁に行われるようなものではないので、サーバサイドの実装はなるべく簡便に済ませて、デザインや挙動の調整に注力すべきでしょう

...と、いうようなこと考えながら作り進めていったところ、最終的に以下のような感じになりました

<img width="876" alt="アーキテクチャ" src="https://qiita-image-store.s3.amazonaws.com/0/106259/3f76f0a1-b53f-0851-987b-9bf8990f5917.png">

段取りとしてはこんなところです

1. 新婦にて、スプレッドシートに参加者の名前と景品名、NGルールを入力する
2. 新婦にて、システムに参加者の画像をアップロードして登録する
3. 筆者にてパーティー当日に抽選をおこない、結果をもとにルーレット画面を表示する
4. 参加者は景品を手にして大喜び！🥳🎁

# 実装

設計と比較しながら、ひとつずつ書いていきます

## Google Spreadsheet

新婦とGoogleドライブを共有し、以下のようなスプレッドシートに値を入力してもらいます
画像ID（１箇所URLて書いてますが...）についてはあとで説明します

**景品一覧シート**
![景品一覧](https://qiita-image-store.s3.amazonaws.com/0/106259/128db961-b764-602b-1058-80fdb0127416.png)

**NGルール一覧シート**
![NGルール一覧](https://qiita-image-store.s3.amazonaws.com/0/106259/6b49df00-1ec7-22c4-b20b-c658684ccb0f.png)

**人物一覧シート**
![人物一覧](https://qiita-image-store.s3.amazonaws.com/0/106259/9fa6cd93-ede4-49ee-a97f-2749d1e42559.png)

上記シートに入力後、「抽選」ボタンを押すと、NGルールに基づいて抽選を行った結果を表示します（[こちら](https://stepism.sakura.ne.jp/adiary/036)のようにすると実装できます）

**抽選シート**
![抽選](https://qiita-image-store.s3.amazonaws.com/0/106259/c68017a5-7c51-ee77-87f1-ac416fa2f37c.png)

## Google Active Script

「表示します」としれっと書きましたが、こちらはGASにより実現します
NGルールシートを参照しつつ抽選をおこなうdrawLots()と、抽選結果を各種一覧データと併せてJSON形式で返却するdoPost()を実装します

```javascript:doPost()
function doPost(event) {
  var response = { error: "error" };

  try {
    var token = SpreadsheetApp.getActive().getSheetByName("抽選").getRange("B1").getValue();
    var s3_credential = SpreadsheetApp.getActive().getSheetByName("抽選").getRange("C1").getValue();
    var s3_bucket = SpreadsheetApp.getActive().getSheetByName("抽選").getRange("D1").getValue();
    var url_prefix = "https://s3.amazonaws.com/" + s3_bucket + "/";
    if(event.parameter.token == token) {
      response = {
        ok: "ok",
        persons: getPersons(url_prefix),
        gifts: getGifts(url_prefix),
        lots: getLots(),
        s3_credential: s3_credential,
        s3_bucket: s3_bucket
      };
    }
  } catch(error) {
    console.log(error);
  }
  
  return ContentService
    .createTextOutput(JSON.stringify(response))
    .setMimeType(ContentService.MimeType.JSON); 
}
```

真面目に書くには貧弱なバージョンのJavaScript&エディタを差し引いても、スプレッドシートのデータ構造とREST APIを手軽に扱えるというGASは相当メリットがあると思います
APIもどことなく馴染みのあるIFですし... ~~やっててよかったVBA~~

あとこれは開発してから知ったのですが、[GASをGit管理できたり](https://qiita.com/rf_p/items/7492375ddd684ba734f8)、[TypeScriptでコードが書けたり](https://tech.actindi.net/2018/09/10/083314)など、環境がだいぶ整ってきているようですので、機会を見つけて触ってみたいと思います

ただ、相当だらだらやっていたため、この段階であてにしていた休みを使い切ってしまい、以降は働きつつ暇を見つけて作ることになってしまいました...

## AWS S3 / Cognito

サーバーサイドはGoogleドライブ&GASでまかなえましたが、
スロット画面および画像、音声ファイルのWeb公開と、画像アップロード機能について、どのように実現するか考える必要があります

[転職先](https://www.hands-lab.com/)でAWSを使う中で、 S3でファイルを外部公開する[静的ウェブサイトホスティング](https://qiita.com/gcsungwoo/items/2e49c3008a04e48581ed)を知り、ファイルサイズ的に利用料もかからず良さそうだったので使うことにしました

ファイルアップロード機能は、[こちらの記事](https://www.selfree.co.jp/2015/06/18/%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%83%AC%E3%82%B9%E3%81%A7javascript-%E3%81%A0%E3%81%91%E3%81%A7%E7%94%BB%E5%83%8F%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95)を参考に、Cognitoで静的ホスティングを行っているバケットへのアップロード権限を確認するように実装しました
利用の際にエンドポイントや接続に必要な情報が必要になりますが、HTMLには埋め込まず、GASのAPIから返却するようにしています

```javascript:接続情報の生成
sontaku.props.s3client = function () {
  AWS.config.region = sontaku.data.s3_credential.split(":")[0];
  AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: sontaku.data.s3_credential
  });
  return new AWS.S3({
    params: {
      Bucket: data.s3_bucket
    }
  });
};
```

# スロット操作/画像アップロード画面

サーバサイドとインフラは大体定まったので、肝心のスロット画面周りを作っていきます
先ほど図で示したように、スロットはパーティ会場のプロジェクタに全画面表示する必要がありますが、それだけだとスタート/ストップの指示を出すのが難しいため、
スロット操作画面を作って、そこからスロット画面の挙動を制御します

メッセージを送る方法は、当初はWebSocketにしたら現地にいなくても遠隔操作できるんじゃ...とか色々考えましたが、最終的にコンソール画面からスロット画面を起動し、windowオブジェクトを直接いじることにしました
ダサいですが、手間もかからず確実性の高いやり方だと思ってます

S3にホスティングされたスロット操作画面からGASのREST APIにアクセスして、抽選結果や参加者一覧、景品一覧の入ったJSONを取得します
こちらの画面の「起動」、「準備」、「スタート」といった各ボタンを押すことで、この後説明するスロット画面の表示状態を制御します

![コンソール](https://qiita-image-store.s3.amazonaws.com/0/106259/a0102f18-b364-a7a2-eb72-58b52f906a4d.png)

また、REST APIへのアクセス時にCognitoの認証IDを受け取っているため、画像アップロード機能についてもエラーとならず動作させられます
なお、画像IDは、[cuid](https://github.com/ericelliott/cuid)を使って発番しています

![画像アップロード](https://qiita-image-store.s3.amazonaws.com/0/106259/c6339148-ca6f-a46f-f0d5-4ef85ff1a0e8.png)
![画像](https://qiita-image-store.s3.amazonaws.com/0/106259/4a0c0422-9c53-be49-1006-b37f2934056a.png)

UIについては、Bootstrap用CSSフレームワークの[Flat UI](http://designmodo.github.io/Flat-UI/)を使ってみたのと、（使いこなせていませんが）SASSを[Koala](http://koala-app.com/)を使いつつ書きました。[前回](https://qiita.com/yktakaha4/items/e535c310f0532798e721)はPingendoに付属していたテンプレートを適当に組み合わせていただけだったので、多少進歩があったものと思います
JavaScriptについては、依然としてjQueryおじさんを抜け出せずにいます...2019年中にはもうちょっとモダンな書き方に追いつきたいと思います

# スロット画面

いよいよ肝心のスロット画面になります。操作画面にてスロット画面表示ボタンを押すと、新婦指定の画像が回りはじめます（Macのファンも凄い勢いで回ります🌪）

![スロット画面](https://qiita-image-store.s3.amazonaws.com/0/106259/8916280b-5d25-dc2e-81fe-8bd0f96ba8df.gif)

スロットのスタートとストップを繰り返しながら、「会社」の「高橋」様に「選べる神戸牛」をプレゼント...みたいな感じで、予め決めておいた抽選結果を順番に画面表示してく...というものになります

スロット操作画面はBootstrapを使いましたが、スロット画面はデザイン用のフレームワークは使わず、素のHTML/CSSとjQueryで頑張るという方針で作りました
単に使いよさそうなものを見つけられなかった、というだけなのですが、今回みたいなユースケースに使えるライブラリやフレームワークってあるんでしょうか...？

## CSS

今回、レイアウトで地味に苦労したのが、「どのような縦横比を想定して要素を配置すればよいか？」ということでした
例えばBootstrapであれば、ブラウザの幅を基準に、あふれる項目を行送り（＝スクロール）させることでレイアウトの整合性を保っていますが、今回の場合だと、各要素をすべて描画エリアに収める必要があります

しかし、本番環境（パーティ会場）のスクリーンがどのような縦横比をしているかは、リハーサルの日まではわかりません
なので、「縦or横の最大幅を１とした相対サイズですべてをレイアウトする」という考え方で要素を配置していく必要があります
ディスプレイやプロジェクターなど、昨今の一般的な表示デバイスは横幅より縦幅の方が短いので、今回は縦幅を基準に横幅を決定する方針で作りました
（説明が下手で申し訳ないですが、以下を見てもらえばおおよそ言いたいことは伝わるものと思います）

**縦幅 < 横幅の場合**
![縦に.png](https://qiita-image-store.s3.amazonaws.com/0/106259/c3b0e48e-100d-f68b-2933-3f673ef409d0.png)

**縦幅 = 横幅の場合**
![正方形](https://qiita-image-store.s3.amazonaws.com/0/106259/ec4ba685-ebe3-29e3-5add-f897c3667831.png)

今回、上記のような画面表示を実現するために、[vh(Viewport Height)](https://www.webprofessional.jp/css-viewport-units-quick-start/)という単位を要所で使っています
`100vh = ビューポートの高さ`というもので、これを使えば、よく使う`%`や`em`では実現しづらいサイズ指定を柔軟におこなうことができます

上記`vh`に加え、`position: absolute;`で花やらリボンやらを表示エリアの端から相対位置で描画していき、画面を作っていきました
画像のサイズや反転などについては、`transform`がだいぶ役に立ちました

```css:花の配置
/* 左の花 */
.flowers-left {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 30%;
  -webkit-transform: scale(-1, 1) translate(5%, 5%);
  z-index: 1;
}
/* 右の花 */
.flowers-right {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 30%;
  -webkit-transform: translate(5%, 5%);
  z-index: 1;
}
```

その他、細かな話だと、文字の装飾（枠線、明滅）はCSS Animationで実現しています。ブラウザでtext-strokeなどの表示感が結構異なり、このため当初ChromeでやるつもりだったものをSafariで動かすように変えたりしました

![screencast 2018-12-24 00-06-40.gif](https://qiita-image-store.s3.amazonaws.com/0/106259/14a7faa9-8578-614d-d57a-806a8d663bf3.gif)

```css:文字装飾
.slot-text {
  font-size: 9vh;
  color: gold;
  -webkit-text-stroke: thin black;
}

.text {
  vertical-align: middle;
  z-index: 50;
  font-family: 'ヒラギノ角ゴ Std W8', monospace;
  font-weight: bold;

  line-height: 1em;
  -webkit-animation: neon 1s ease-in-out infinite alternate;
}

@keyframes neon {
  from {
    text-shadow: 0 0 1px white, 0 0 2px white, 0 0 3px white, 0 0 4px lightyellow, 0 0 5px lightyellow, 0 0 6px lightyellow, 0 0 7px lightyellow, 0 0 8px lightyellow;
  }

  to {
    text-shadow: 0 0 2px white, 0 0 4px white, 0 0 6px white, 0 0 8px lightyellow, 0 0 10px lightyellow, 0 0 12px lightyellow, 0 0 14px lightyellow, 0 0 16px lightyellow;
  }
}
```

花、リボンやプレート、背景のカーテンは[Illust AC](https://www.ac-illust.com/)という素材サイトより拝借しました

https://www.ac-illust.com/main/detail.php?id=1023007
https://www.ac-illust.com/main/detail.php?id=718442
https://www.ac-illust.com/main/detail.php?id=860468

## JavaScript

スロットの仕組みは、jQueryのカルーセルライブラリの[Slick](http://kenwheeler.github.io/slick/)をカスタマイズしてなんとかしました。これがなかったら多分頓挫してました...それぐらい色々いい感じにやってもらえて助かりました

また、スロットの回転時にプレートをぼやかした方がそれっぽそうだったので、同じくjQueryフレームワークの[Foggy](http://nbartlomiej.github.io/foggy/)を使いました

```javascript:ライブラリの使用イメージ
// Slick
$(".slot-wheel").slick({
  arrows: false,
  infinite: true,
  slidesToShow: 3,
  slidesToScroll: 1,
  centerMode: true,
  variableWidth: true,
  autoplay: true,
  autoplaySpeed: 0,
  speed: 1000,
  pauseOnFocus: false,
  pauseOnHover: false,
  cssEase: 'ease',
  useCSS: true,
  useTransform: true
}).slick("slickPause");

// Foggy
$(".slot-wheel").foggy(false);
```

![スロット回転](https://qiita-image-store.s3.amazonaws.com/0/106259/516628b4-e63f-5e2c-e548-8dba061f411f.gif)

あたりの際に背景をおめでたい感じにしたかったので、[pow.js](http://obadger.com/pow/)を使って紅白を入れ替えた背景を２枚作り、パカパカさせて動きをつけました

![screencast 2018-12-23 23-56-19.gif](https://qiita-image-store.s3.amazonaws.com/0/106259/9ce56768-896d-56a6-2b4b-ea0ca3b9ba45.gif)

```javascript:pow.js利用イメージ
// pikaって...
var pika = function (selector, type) {
  var ray = type ? "red" : "white";
  var bg = type ? "white" : "red";
  $(selector).pow({
    rays: 32,
    rayColorStart: ray,
    rayColorEnd: ray,
    bgColorStart: bg,
    bgColorEnd: bg,
    originX: "50%",
    originY: "50%"
  });
};
pika(".pikapika.fore", false);
pika(".pikapika.alt", true);
```

その他、背景で星がキラキラしてるのは、[こちら](http://jsdo.it/nao_k116/rx27)を参考に実装しました

あと、作っている途中にエンドユーザ（新婦）から音を鳴らして欲しいと言われ、[素材サイト](https://soundeffect-lab.info/sound/anime/)から頂きつつ[audio要素](http://www.htmq.com/html5/audio.shtml)を使ってなんとかしました（コードは参考にならないレベルなので割愛します...）

---

技術的な話を中心に色々書きましたが、この間に少し作っては新婦に確認してもらうよう連絡し、なるべく齟齬が出ないように努めました

# 運用

画面表示確認のリハーサルを一回挟み、当日はすぐにやってきました
一般参加者より早めに会場入りし、機材を繋げて待ちます...

会の参加者は、最終的に80名ほどになっていました...
パーティが始まっても酒の一滴も飲まず、料理も口にせず、
知人と話せども緊張で要領を得ず、ただ己の順番が来るのを待ちます...

パーティも半分を過ぎた頃、ようやく自分の番がやってきました...
新婦が「ルーレット、スタート！」だとか「ストップ！」とか言うたびに、スロット操作画面のボタンを一つずつ押していきます...

うわあ...マジで動いてるよ...😨

![本番](https://qiita-image-store.s3.amazonaws.com/0/106259/31b6bfc0-676f-1ac9-ba40-8389194012a1.png)

みんなめっちゃ見てるし...🤢（当たり前だ）
時折会場が盛り上がる度に肝が冷え、変な笑いが出ます...

![本番２](https://qiita-image-store.s3.amazonaws.com/0/106259/55636b08-edc4-62b8-0cdb-f32ff952d6d9.png)

その後、はよ終わってくれという思いを込めて無心でボタンを押し続け、
結果的には大きなトラブルもなく、何とか持ち時間を無事やりきったのでした

終わった後もしばらく放心していたのですが、
残っていた冷えたマカロニ的なものとビールがすげえうまかったのを覚えています...🍻

書いてみるとほとんどあっという間という風ですが、実際の時間感覚もこんな感じだったように思います⏳

# 所感

思ったことを箇条書きで...

- 音はすげえ大事🎷
    - 家でテストしているときは無音でもそこまで気にならないのですが、実際に現場で動かすと、人は音でゲームの状態（ドラムロールで抽選中とか、シンバルがなったらスロットが止まったとか）を認識しているように思いました。音がちょっと出遅れると緊張が走ったりして...
    - 同じ意味でBGMも大事です。万一トラブルが起きた時に、無音で参加者を待たせて場が冷える、ということは避けねばなりません。トラブルがなくても、ゲームの進行中にもたついたりした時に、背景で何かしら流れているとだいぶ事故感が薄れるように思いました
    - 今回は、Amazon Musicで「ジャズ　楽団」みたいなワードで検索し作ったプレイリストをループ再生してました

- 画面を暗転させる時は、画面端にマークか何かを表示しておくとよい
    - 会場にもよるものと思いますが、今回はHDMIでプロジェクターにつないで投影という形でした。当日までリモコンを手元に置いておけるかわからなかったので、初期表示時に暗転→ボタン制御で表示という形にしたのですが、HDMI側とアプリ側のどちらで表示を切っているかわからず、不安になる場面がありました
    - 画面暗転時に、左上に赤とか緑で●を表示しておくとかすれば、わかりやすくなったかなと思います

- 異常系のケースをちゃんと考えておこう🚨
    - 余興の途中に「当選者が確定したがタバコを吸いに行ってていなかった」ということがおきたのですが、司会の方が機転を利かせて当選者の知人に代わりに受け取ってもらうよう促してくれて命拾いした...ということがありました
    - くじ引きのようにその場で再抽選する、ということが不可能な仕様にしていたのがよくなかった、という話でもありますが、事前に伝えておけば運用でうまくカバーできることだとも思うので、SE的に言えばコンチプランって大事だなあ...と思った次第です

- ユーザの要望はちゃんと聞いておこう
    - 新婦に確認してもらってる中で、「ルーレットの出目を無理やり動かすパターンを作ってほしい」と言われていたのですが、当時それ以前にSlickをうまく動かせておらず（一つずれて止まったりとか😇）、「できたら対応する」的なことを言ってうやむやにし、結局やらずじまいで当日を迎えました
    - 余興が終わった後に友人（思ったことなんでも言ってくるやつ）に話しかけられ言われたのは「最初はよかったけど単調で飽きた😡」ということでした
    - 仕様を出している当事者が指摘することは、高い確率で他の誰かも指摘するという好例だなと思ったのと、そうした（コードベース上では）ちょっとした改善でユーザの満足度が大きく上がることも往々にしてあるだろう、と実感しました

- （望むなら）お金のことを事前に決めておこう💰
    - パーティが終わった後、一緒に参加した友人と２次会やらカラオケやらに行っていると新婦がやってきて、寸志をもらいました
    - 個人的には、人目に触れるものを作るいい機会と思ってやっていたことなので謝礼をもらうのは特に考えてなかったのですが、「もしもこれが個人事業主で、受注した仕事にもらった報酬だとしたら...」と考えると、金額を提示して、それに見合った仕事をする、というプロセスは（スキルを伸ばすためにも）結構大事だなと思いました
    - 今後引き受け仕事をするときは、少なからずそうしたことも考えていきたいと思います

# ソース

https://github.com/yktakaha4/otanoshimi-slot

APIのコードが断片的、かつ画像や音声素材を削除（0バイトのファイルに置換）しているため、そのままだととても動かせないのですが、一応Githubにあげてみました:octocat: