🪑

同窓会でランダムに席替えをするアプリを作りました

2025/01/14に公開

経緯

こんにちは、らっじです。
20歳になったので、今月は成人式やら同窓会やらで盛り上がっておりますが、自分は中高一貫校出身なので、成人式のあとに中高の同窓会に参加してきました。
さて、同窓会をやるにあたって、同窓会のだいたい1週間前に幹事の友達から連絡が来まして、

「同窓会の途中でランダムに席替えしたいから、そのようなプログラムを作ってほしい」

と声をかけてもらいました。
自分は普段趣味の個人開発でWebフロントエンドを触っているので、なんかそれを活かして作れたらいいな〜ということで、1週間かけてWebアプリを作りました。

要件

今回幹事から提示された要件は以下の通りでした。

  • 64人参加する
  • 途中で2回席替えを行う
  • 最初の席もランダム

これに加えて、同窓会をより盛り上げるために、かつ効率よく制作するために、自分で追加で以下のような要件を考えました。

  • 全員が必ず移動する(同じ席に留まる人が出ないようにする)
  • 直前まで次の席が予測できないようにする
  • バックエンドを使わずに静的サイトで実装する
  • スマホのみをターゲットとする

特に、下から3番目の「直前まで次の席が予測できないようにする」というのは結構こだわりました。

実装

技術選定

まず、何を使って作るかを検討しました。
今回は以下のような構成を採用しています。

  • bun.js (Javascriptランタイム)
  • Svelte (フロントエンドフレームワーク)
  • Cloudflare Pages (静的サイトホスティング)

期間が1週間しかないため、自分が得意な分野でやるしかありませんでした。本当はもっといいやり方があるかもな〜と思いながら選びました。

席替えの方法

フロントエンドだけで全員の席を重複なくシャッフルするために、次のような方法を考えました。

まず、座席に番号を振り、その番号の配列を持っておきます。これは全員のページで共通のものとします。
次に、参加者に番号を振ります。i番目の人は座席の配列のi番目にある番号の席に座るというようにして、座席の配列をシャッフルすれば、重複することなく席替えをすることができます。

この方法を実現するためには、配列をランダムに、かつ各参加者のデバイスで同じ結果が出るようにシャッフルする必要があります。

シャッフルする際に乱数を使いますが、コンピューターで使える乱数は何らかのseed値を基に生成される疑似乱数で、同じseed値を与えると同じ順番で乱数が生成されるという性質があります。

普段は時間ベースのseed値を与えることで、実行の度に違う順番の乱数を生成しますが、今回のように再現性のある乱数が欲しい場合は、明示的に同じseed値を与えてやればいいです。

jsだとseedrandomというパッケージを追加することで可能です。

bun i -D seedrandom

配列をシャッフルするために、Fisher-Yates shuffleというアルゴリズムを採用しました。

以下のような処理でシャッフルできます。

import seedrandom from 'seedrandom'

function shuffle(seed) {
  const newArr = [...arr]

  function isShuffled() {
    for (let i = 0; i < arr.length; i++) {
      if (arr[i] === newArr[i]) {
        return false
      }
    }
    return true
  }

  const rand = seedrandom(seed)
  while (!isShuffled()) {
    for (let i = arr.length - 1; i > 0; i--) {
      const j = Math.floor(rand() * (i + 1))
      const tmp = newArr[i]
      newArr[i] = newArr[j]
      newArr[j] = tmp
    }
  }
  arr = [...newArr]
}

shuffle関数の引数のseed値を使ってシャッフルするので、全員のデバイスで同じ結果を得ることができます。

また、席替え前の状態と比較して、席を移動していない人がいなくなるまでシャッフルし続けるようになっています。

席の表示

座席表は、幹事からもらっていたデータをもとに、webアプリに組みこみやすいように画像を作成しました。

Inkscapeで見た座席表

Inkscapeを使用し、svgで元になる座席表を作成しました。座席を全てグリッドにスナップさせて作ることで、グリッド数を座標として扱えるようにしました。

これをpng画像として出力し、それとは別に座席の位置を定義したjsonファイルを以下のように作成しました。

{
  "size": [27, 38],
  "seats": [
    [2, 6, 4, 8],
    [2, 8, 4, 10],
    [2, 10, 4, 12],
    [4, 6, 6, 8],
    [4, 8, 6, 10],
    [4, 10, 6, 12],
    ...
  ]
}

sizeの値は画像の幅と高さをグリッド数で示していて、seatsの中身はそれぞれの席の[左上のx座標, 左上のy座標, 右下のx座標, 右下のy座標]となっています。

このようにデータを定義することで、当日キャンセル等で使用する座席に変更があった際に対応しやすいようにしています。

あとは、このデータを元に画像上に四角形などを置いて現在の席を示すようにすればいいです。

席を示した画像

これはHTMLのdiv要素で四角形を作成し、CSSで親要素にposition: relative;、四角形にposition: absolute;を指定して、あとはtopleftで座標を指定することで目的の場所に表示しています。

また、席替えで席を移動するときに、現在の席から新しい席への移動を視覚的にわかりやすく表示したかったので、矢印を追加しました。

席移動

これは、元の席の中心の座標と、移動後の席の中心の座標を使って、2点間の距離と、x, yそれぞれの差からアークタンジェントで角度を求め、htmlのdiv要素で作成したその距離分の矢印を目的の位置に配置、回転させることで表示しています。(これは言葉で説明が難しいのでコードを読んでもらったほうがいいかもしれません)

角度を求めるときに、JavascriptのMath.atan2(y, x)という関数を使用しました。
これは、y座標の差とx座標の差を与えると-\piから\piの範囲で角度を返してくれます。

最初、Math.atan(x)という関数で書いていましたが、こちらはそのままy = Tan^{-1}(x)の関数で、-\pi/2から\pi/2の範囲で角度を返してくれます。

今回のような使い方をする場合、atan()だと符号を自分で分岐して実装したりする必要があり、途中で友達がatan2()の存在を教えてくれたので、atan2()で書き直しました。

成果物

完成したwebアプリは以下のようになっています。

https://sekigae.laddge.net/?idx=0

アプリのスクリーンショット

席替えボタンを押すと、seed値を選択する画面が表示されますが、当日の席替えのタイミングで適当な人にサイコロを振ってもらい、参加者がその値をここで入力することで、席がシャッフルされるようになっています。

これにより、要件の一つである直前まで次の席が予測できないゲーム性を担保することができるようになっています。

他にも、リロード時にデータを保持できるようにlocalStorageを使用しています。
これは、右上の席替え回数のボタンをタップすることでリセットできます。

URL配布の方法

今回のサイトは、参加者それぞれに番号を振って、それをクエリパラメーターに入力することで動くようになっていますが、同窓会に来た人に番号付きのurlを配るために、QRコードを使用しました。
(QRコードは株式会社デンソーウェーブの登録商標です)

これも、ただ紙に印刷して渡すのもいいですが、せっかく普段3Dプリンターを触っているので、3Dプリンターでカードを作って渡せたら面白いと考えました。

今回同窓会に参加する人の中に、自分と同じく電気通信大学の工学研究部に所属している友達がいるので、彼にこの話をしたところ、QRコードを載せたカードを印刷するための3Dデータを作ってくれました。

彼がそのときの知見を残してくれているのでよかったら見てみてください。

https://kat0h.notion.site/QR-3D-177ffc62bce780df9ae3d65e72430338

これを工学研究部にある多色印刷が可能な3Dプリンターで印刷して同窓会に持っていきました。

印刷したQRコードのカード

結果

実際に同窓会でこのwebアプリを使用し、スムーズに席替えを行うことができました。

また、仲の良い友達からは、「これ自作ってマジ??」と褒めてもらえて嬉しかったです。

今回作成したサイトのソースコードはGitHubにて公開しています。
よかったらそちらも見てみてください。

https://github.com/laddge/sekigae-app

Discussion