はじめまして(?)。taiyohと申します。
幾つか事業部を渡り歩いた後、現在はLobiにて新規機能の開発などを行っております。
皆様は3/3発売のNintendo Switchの予約はお済みでしょうか。僕は早速1/21の午前中に近所の電器店に駆け込んで予約をしました。ゼルダ、Splatoon2、ゼノブレイド2と、既にやりたいソフトがいくつかあるので大変楽しみです。2017年も何とか生きていけそうです。どうぞ宜しくお願いします。
さて、昨年の2016年末ですが、Lobiアカウントを利用したトーナメントサイトというものをリリースしましたので、そのちょっと技術っぽい観点についてお話させていただきます。なお、技術っぽいと書きながら実装やインフラ成分は低めです。その点予めご了承ください。
Lobiトーナメントはゲーム大会をより簡単に開催・参加できるようにしたサービスです。大会のエントリーから大会終了まで、スマートフォンさえあればすぐに使うことができます。トーナメント表、対戦の組み合わせ、相手とのチャットコミュニケーションなど、オンライン、オフラインに限らず、ゲーム大会に必要なさまざまな機能が揃っています。1/28の土曜には12月に次いでハースストーンの第二回大会も行われ、今回も進行不可能になる状況もなく、参加者の方々には引き続きお楽しみいただけたようです。
第一回ハースストーンLobi杯 - トーナメント表 - Hearthstone - Lobiトーナメント
目次:
トーナメントの作り方について
トーナメントといえば、1回戦から徐々に勝ち上がり、決勝戦で勝つと優勝という、あのトーナメント表が真っ先に浮かぶと思います。
第2回スプラトゥーン甲子園 関東地区大会DAY1が終了した。
— Splatoon(スプラトゥーン) (@SplatoonJP) 2017年1月21日
一足先に3回戦へ進出を決めたのはご覧のチームだ。闘会議での活躍にも期待したい。
関東地区予選は明日も実施されるので、引き続き観戦いただきたい。https://t.co/YfpAAnVJUT pic.twitter.com/iEGr5KOg28
(こちらの写真はイメージです)
実はこのサイトの実装に入る前に、どこかにトーナメント表をプログラムとして作った人がいないか、Googleで検索するところから始めたのですが、自分の求めるサイトや記事が見つかりませんでした。なのでこのエントリでは、作り方というか、データ構造の考え方について多少多めに文字数を割きたいと思います。
さて、このトーナメント表をデータとしてどう表現するかといいますと、まず考えるべきは木構造です。ただ、安易に木構造をDBに保存することを考えてしまうと、SQLアンチパターンの第二章に記述されている「ナイーブツリー」に該当してしまいます。
SQLアンチパターンに記載されている解決方法は、以下のことが考慮されているはずです。
- 任意のノードに新たにノードが追加・削除されることを想定する
- 任意のノードの親や子のノードをリストアップできる
- 任意のノードから見て別のノードの深さを調べることができる
ただ、トーナメントの木構造は以下の特徴があります。
- 木構造の中でも二分木に該当する
- あとからノードが追加・削除されることがない
- 条件が揃うと完全二分木になる
完全二分木であれば、SQLアンチパターンに記載された方法でなくても、配列でデータを保持しながら考慮すべき点を全てクリアすることができます。というのも、配列のインデックス番号を使えば、任意の2つのノード間に親子関係があるかどうかや、同じ深さのノードかどうかも簡単な計算で算出することができるからです。設計がシンプルにできるだけでなく、データ量も大幅に抑えることができます。これを使わない手はないと思いました。
ただ、一つ問題があり、トーナメントの参加者が完全二分木を構成できる人数になるとは限らないということです。参加者を2n人にするために抽選で落とすなどしたくはありません。
とはいえ、完全二分木を採用することのメリットはとても大きいので、今回このトーナメントサイトでは、ダミーのプレイヤーを大会毎に紛れ込ませておいて、足りない分の人数はそのダミーのプレイヤーで埋めることで、必ず完全二分木の構造となるように調整することにしました。ダミーのプレイヤー同士の対戦は表示する側で調整すればよく、シードも同様に表示方法を工夫してもらうことで、この問題を乗り切ることにしました。
こちらが、出来上がったテーブル構成の一部です。試合テーブルの「シーケンス番号」カラムは、決勝戦(=根ノード)を1として、幅優先の順序で番号を振っています。また、対戦相手1と対戦相手2がそれぞれダミーのプレイヤーだった場合は試合のステータスは「未決定」となり、片方がダミーのプレイヤーだった場合は「シード」というステータスとなります。
Wikipediaのトーナメントのページを見てみますと、よく見知ったトーナメントだけでなく、実は様々な方式があることがわかります。これらの方式を全て網羅するようなトーナメントサイトを作ることはできません。まずはPO(プロダクトオーナー及びプロジェクトオーナー)達とよく相談をして、どういうトーナメント方式にするのかを決めるということが何より重要です。今回の場合「勝ち残り方式」で「シングルイリミネーション」であったからこそ、ここまでの設計上の割り切りができました。
テスト環境について
CIはCircleCIを使用しております。現在弊社にて稼働中のプロジェクトの多くはJenkinsを使用しており、実績もありますが、規模がそこまで大きいサイトではないので、試験的な意味も兼ねてCircleCIを選択することにしました。ここでは、ちょっとハマったポイントについて共有したいと思います。
本番のRDBはAmazon Auroraを使用することにしていたので、できればクライアント側のDBのドライバはバージョン5.6としてビルドしたもので統一したいという気持ちがありました。
ただ、調べてみたところ、このエントリの執筆時点でCircleCIで提供しているMySQLは5.7のみとなっています。検索してみて解決方法を書いている方が何人かいましたが、基本的には「既にあるものをアンインストールして必要なものをインストールし直せ」というものです。ただ、自分の場合、どうにも食い合わせが悪かったのか、1日2日では解決しそうにありませんでした。
散々ハマった挙句最終的に採用したのが、dockerのMySQL5.6イメージをCircleCI上でdocker pullしておき、テスト実行時にそのコンテナを起動して接続する、という方法でした。諸事情で今回のサイトはPerlを使って構築したのですが、PerlであればTest::Docker::MySQLというCPANモジュールがあるので、dockerでMySQLコンテナを立ち上げる際の障壁は低かったです。ただ、これを案件にフィットする形で一部改修を加えて使用しています。
実際に使用しているcircle.ymlの中から、テスト実行前の依存モジュール等のセットアップ部分を切り出しました。
dependencies: cache_directories: - ~/perl-5.24 - ~/docker - local - assets/node_modules pre: - | sudo apt-get remove -y 'mysql-*' sudo apt-get autoremove -y sudo apt-add-repository -y 'deb http://ppa.launchpad.net/ondrej/mysql-experimental/ubuntu precise main' sudo apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libmysqlclient-dev - | mkdir -p ~/docker if [ -e ~/docker/mysql-56.tar ]; then docker load < ~/docker/mysql-56.tar else docker pull mysql:5.6 docker save mysql:5.6 > ~/docker/mysql-56.tar fi - | mkdir -p ~/perl-5.24 if [ -z "$(ls -A ~/perl-5.24/)" ]; then wget https://raw.githubusercontent.com/tagomoris/xbuild/master/perl-install -O ~/perl-install chmod +x ~/perl-install ~/perl-install 5.24.0 ~/perl-5.24 fi carton install --deployment
CircleCIはまっさらなコンテナから都度環境を作成するので、データやツール等が次のテストに持ち越されることがないので、依存が少なく済むのでとてもよいですね。ただこれは諸刃の刃で、テストの度に構築処理が走るので、テスト自体は一瞬で終わるのに構築に数分かかる、ということもあります。そこが気になる方は、テスト用のイメージを作成しておけばその分のオーバーヘッドは減るかと思います。ちなみに今回のプロジェクトではそこまでは行っておりません。現状、proveによるテストはおよそ3分半ほどに対して、上記YAMLによる環境構築も同じくらいかかっています。
開発環境について(docker周り)
テスト環境でdockerを使い始めたので(CircleCIが元々dockerベースなのはおいといて)、開発環境も手を入れようと思い、docker-composeで環境を組んでみることにしました。Docker Hubにperlのイメージが存在していたので、これを導入して開発用のコンテナを作成しています。開発スピードがだいぶ優先されていたので、Dockerfileにはモジュールのインストール周りの記述はせず、docker-composeでの起動時に volumes
を使ってgit cloneしたディレクトリをそのままマウントする形にしています。
以下に、プロジェクトで使用しているdocker-compose.ymlを当たり障りのない範囲で載せます。
version: "2" volumes: mysql: redis: services: mysql: image: mysql:5.6 ports: - 3306:3306 environment: MYSQL_ALLOW_EMPTY_PASSWORD: 1 volumes: - mysql:/var/lib/mysql redis: image: redis:3.2 ports: - 6379:6379 volumes: - redis:/data node: image: node:6.9.1 working_dir: /app volumes: - .:/app web: &app build: . image: hoge working_dir: /app # /app 以下にリポジトリを設置している ports: - 5050:5000 volumes: - .:/app - /var/run/docker.sock:/var/run/docker.sock depends_on: - mysql - redis links: - "mysql:hoge-mysql" - "redis:hoge-redis" stdin_open: true command: "plackup ****" admin: <<: *app ports: - 5000:5000 command: "plackup ****"
docker周りで個人的に一番驚いたのは volumes
の挙動です。散々調べた結果、これはホストマシンのパスだけでなく、Linux環境の元イメージのパスも共有できるということでした。Windows上でdocker-composeを動かした時、 /var/run/docker.sock
がちゃんと共有されて内部からdockerコマンドが問題なく動いたのを見た時は、目を疑ってしまいました。当然、Windows環境には /var/run/docker.sock
は存在しておりません。知らないと怖いですね。(「Dockerでホストを乗っ取られた」という恐ろしい投稿もあるので、取扱に注意が必要です)
docker-composeを組んだ副産物として2点ほどよかったことがあります。
- フロントエンドエンジニアの使うnodejsのバージョンもdocker-composeに収めることができた
- Docker for Windows上でだいたい動く
前者については、フロントエンドを担当しているエンジニアがgulpによってCSSやJSのビルドをを行っている都合でnode.jsの環境構築が必要だったのですが、nodeのイメージをdocker-compose.ymlに入れておいたので、ローカルの環境を変更することなくビルドの作業ができるようになりました。
後者は、Windows10だとPro版でないと動作しません。また、「だいたい動く」と書いたのは、「 docker-compose run
が動かない」という問題があり、仕方なくコンテナを起動しっぱなしにしておいて、 docker exec
で中に入る必要があります。ただ、どうやらこれはdocker-composeのバージョンが1.8までの話で、1.9では解消するらしいという話をこのエントリを書いている最中に見つけました。
Docker for Windowsユーザはご参考までに。
現状では開発環境しかDocker化されていないですが、ゆくゆくはECSに置き換えていきたいと考えている今日このごろです。
まとめ、というかおまけ
- トーナメント表を実装する際は、仕様を適切にシュリンクさせないと死亡するので注意しましょう
- CircleCIは環境構築の時間が長いのでなんとか頑張りましょう(?)
- Docker for Windowsは思った以上にイケる
最後に、昨年の12/23と今年は1/28にこれまで2回大会が行われたのですが、大会開催中は特に不正もなく、ユーザからシステム不具合や運営方法でクレームをつけられることなく、和やかに終了しました。終了後アンケートをとってみたところ、「また参加したい」という反応が多く、サイトのシステムに対しても「クレーム」ではなく「改善案」を挙げてもらえるという、とても貴重な体験ができました。この場を借りて、改めて参加者の方々に御礼申し上げます。