はじめに
AtCoderなどの競技プログラミングのコンテストに参加しているyunixです。
来年に開催されるマスターズ選手権、楽しみですね。マスターズ選手権は28歳以上の2~3人の団体戦で、時間が6時間のヒューリスティックコンテストになるそうです。
このコンテストの大きな特徴の一つはビジュアライザが提供されないことが明言されていることです。
ヒューリスティックコンテストではビジュアライザを使用して考察することがとても大事なステップになります。
従って、参加者自身でビジュアライザを開発することになると思うのですが、ビジュアライザの開発の準備をしていなければ
- 開発に時間がかかってしまう
- 使用感や機能がいまいち
などの点で辛い戦いになってしまいそうです。
そこで、
- いつものAHCと同じような使用感でビジュアライザを使える
- コンテストの問題に合わせて最低限のところだけを編集すれば使えるようになる
ようにするために、ReactとRustを使ってビジュアライザ開発用のテンプレートを作りました(Reactに書き換えてはいるもののほぼ公式のビジュアライザを移植しただけなので大したことはやっていないのですが...)。
基本的にReactの部分は触らずに、Rustで問題ごとのビジュアライザや入力作成機能を作ればいつもの感じでビジュアライザが使えるようになっています。
この記事では作成したテンプレートの解説をしようと思います。(使用方法はリポジトリのreadmeを参照して下さい)
github.com
追記(2024/3/2)
最近のAHC(AHC030など)では乱数生成などのクレートのバージョンが新しくなっていて、上記のリポジトリのCargo.tomlの内容では提供されたツールをコピペしても文法エラーになる可能性があります。
最近のAHCの配布ツールに合わせてクレートのバージョンを変更して使ってください。
AHC030では以下のもので配布されたツールをコンパイルすることができました。getrandom = { version = "0.2", features = ["js"] }の箇所が多分重要で、wasmにするのに新たにこの依存関係が必要です。
[package]
name = "rust"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.89"
getrandom = { version = "0.2", features = ["js"] }
rand = { version = "=0.8.0", features = ["small_rng"] }
rand_chacha = "=0.3.1"
rand_distr = "=0.4.3"
itertools = "=0.11.0"
proconio = { version = "=0.3.6", features = ["derive"] }
clap = { version = "4.0.22", features = ["derive"] }
svg = "0.9.0"
delaunator = "1.0.1"
web-sys = {"version" = "0.3.44", features=['console']}
サポートしている機能
AHCのビジュアライザで提供している機能を参考に作りました(レイアウトも大体同じにした):
- output欄に入力した出力に対応する画像・スコアを表示する機能
- seedを指定して入力を作成する機能
- 複数のアウトプットを含むフォルダをアップロードして、各出力の内容を表示する機能
- 複数の入力を作成してzipでダウンロードする機能
- png・gifで保存する機能
- ターンがある場合にはターンごとにスライダーで表示する機能
いつものビジュアライザの機能をほぼそのままReactに持ってきたような感じです(そのまますぎて怒られないかな...?)。
特に画像や動画の保存機能などについては処理をかなり参考にしました。
ビジュアライザの機能の何がコンテストごとに固有のものか?
前述のような機能を実現しようと思ったときに、何が特定のコンテストに固有の処理で、何がコンテストによらずに共通で使えるかということを考えることは重要です。
コンテスト中にビジュアライザを作成しなければいけないため、極力使いまわせるところは使い回して、コンテストごとに固有の事情に集中できるようにしたいためです。
例えば、スライダーを動かしたり、フォルダのアップロード機能、入力のダウンロード機能などは使いまわせそうです。
一方で、入力を作成する機能や、出力に応じてスコアをつけたり画像を出力する機能はコンテストごとに作らなければいけません。
このように考えると、以下の3つの機能をコンテストごとに実装すれば良さそうだという結論に至りました(AHCで提供されているビジュアライザは大体そうなっているので踏襲しただけとも言えるのですが...):
- seedの値が与えられたときに、入力ケースのstringを作成する機能
- 入力・出力から最大のターン数 (スライダーを使う場合など)を返す機能
- 入力・出力・スライダーのターンを指定したときにスコア・表示する画像を返す機能
これら3つの機能をRustの関数として記述して、WebAssemblyでJavaScriptから呼び出せるようにするだけで、大体いつものビジュアライザと同じような使用感で使えるようになります。
React側のフロントエンドについては、何か編集したい事情(ほかの画像を生やしたい、入力を増やしたいなど)がない場合には特に触る必要はありません。
使用している技術
フロントエンドをReact、コンテストごとに固有の部分をRustで作成してWebAssemblyでJavaScriptから使用できるようにしています。
このようになっている理由はいくつかあり、
- AtCoderのコンテストではローカルテスタや入力作成機能がRustで提供されることが多いです。そのため、マスターズ選手権でこれらが提供される場合(されるか現時点では未定)、これらの処理をそのまま再利用できると大変有利になります。
- AHCのビジュアライザがこの構成になっているので踏襲した
- 個人的にRustとWebAssemblyを勉強したかった
従って、コンテストごとに固有の処理の部分を必ずしもRustで書く必要はなく、何で書いても大丈夫ではあります。
特に、インタラクティブな操作がビジュアライザに必要な場合にはReactで処理を書いた方が良い可能性もあります。必要に応じて書き換えましょう。
RustのWeb Assemblyについて
RustのWebAssemblyを扱うために、wasm-bindgenというモジュールを使っています。
Rust側で
#[wasm_bindgen]
pub fn gen(seed: i32) -> String {
"結果".to_string()
}
このように"#[wasm_bindgen]"をつけて関数などを記述し(構造体とかでも良い)、wasm-packを使用してRustをwasm化
wasm-pack build --target web
すると、JavaScriptの方でこのように呼び出すことができる関数が生成されます。
const inputText = gen(seed);
基本的には、
https://github.com/yunix-kyopro/visualizer-template-public/blob/main/wasm/src/lib.rs
ここの3つの関数(gen, vis, get_max_turn)を編集して、JavaScriptから呼び出すことができるようにします。
ビジュアライザ開発のサンプル
前述したように、lib.rsの
- gen関数: seed値を与えられたときに入力ケースのstringを返す関数
- get_max_turn関数: 入力・出力のstringを与えられたときに最大のターン数を返す関数
- vis関数: 入力・出力・ターン数を与えられたときに、スコア・エラー文・svgの画像のstringをRetという構造体で返す関数
をRustで実装すればいいです。
ここではyukicoder score contest 2を題材にしてこれらの関数を実装しました。
実装はこちらのブランチにあります:
GitHub - yunix-kyopro/visualizer-template-public at yukicoder-score-contest-002
mainブランチから変更されたファイルは以下の2つです:
util.rsに問題の固有の処理(入力生成や画像生成など)を書き、lib.rsからそれを呼び出す形式で実装をしています。
util.rsの処理は、普段配布されているローカルテスタ(のlib.rs)の処理の内容をかなり踏襲していて、
- Input, Outputの構造体、特にDisplayやproconioを使ってstringから構造体を作成するところなど
- vis関数、特にsvgを作成する処理
などは記述方法をかなりそのまま持ってきています。svgの記述方法などは特に参考になり、作りたいビジュアライザに近い回のローカルテスタのvis関数の内容を参考にすると良いと思います。
これらだけを編集して、wasmをビルドすると、yukicoder score contest 2の内容のビジュアライザを使用できます。
こちらにseed=0のサンプル出力を用意したので、動かしてみましょう。
開発に関する注意など
場合によっては一部の処理を実装することをサボっても良いかもしれません。
例えばサンプルケースがファイルとして配布されるようなときには、gen関数の実装はサボることも十分に考えられます。
また、スコア計算も実装がめんどくさくなりがちなので、サボってビジュアライズ部分だけを作るなどのこともあり得ると思います。
ビジュアライザ筋トレをやろう
ビジュアライザを短期間のコンテストで開発するとなると、相応に準備と練習をしておいた方が良いです。
ここではこのツールを使う練習の方法について記述したいと思います。
動かすために必要な環境などはリポジトリのreadme.mdを参照してください。
ステップ1: サンプルの実装のブランチを動かす
yukicoder score contest 2の他にも、chokudai contest 005の実装を用意しました。
これらのブランチをチェックアウトして、
cd wasm && wasm-pack build --target web --out-dir ../public/wasm && cd ..
を実行してwasmをビルドし、
yarn dev
でビジュアライザを動かしてみましょう。
動いたらutil.rsのvis関数の内容を変えて、表示される内容を変えてみると良いかもしれません(その際にはwasmのビルドを忘れないようにしてください)。
ステップ2: 既存のAHCのビジュアライザを再現する(ローカルテスタを再利用)
次に、mainブランチのテンプレートから開発を始めて、既存のAHCのビジュアライザを再現してみましょう。
既存のAHCのビジュアライザを再現することで、どこで何を書けばいいのかを把握しましょう。
触るべき場所はlib.rsとそこから呼び出すモジュール(util.rsなど)です。
AHCのローカルテスタのlib.rsの中身を再利用するととても簡単にできます(引数などを調整しないといけないことはありますが、基本コピペをして、lib.rsから呼び出せばよい)。
ステップ3: ヒューリスティックコンテストのビジュアライザを一から実装する
コンテストで使用することを想定して、1からビジュアライザを作ってみましょう。
1からとは言っても処理を踏襲できるところは多いはずです。
これを素早くできるようになれば実践でも実用的になるはずです。
- CICDと組み合わせてどこかにホスティングしてチームメンバーが使えるようにするのも良いでしょう。Github Pagesなど? (料金かかるけど...)
- スコア計算を書くところは、Rustに慣れていないと結構バグらせてしまうかもしれません。ビジュアライズ部分は基本的にとても簡単なので、スコア計算を無視してビジュアライズだけしてもいいかもしれません。
- reactを使っていますが、実はwasmを使うところのアイデアや関数の切り分けのアイデアはAHCのビジュアライザとほぼ同じです。従って、公式からビジュアライザのhtmlをダウンロードしてきて、wasm部分を同様に入れ替えることで、reactを使わずに同じことをやることもできます。
- 何がターンとなるのかはコンテストごとにまちまちです。例えばAHC006だと複数ルートを出力して、それをターンごとに動かす機能の方がありがたいと思います。この辺りはRustの方の処理でターンが何かを解釈することで対応できるかなと思います。
作ったビジュアライザはwebを経由してチームメンバーと共有できると楽です(ローカルにcloneしてきて動かすでも悪くはないと思うのですが...)。
この際に気をつけないといけないことは、
- 認証機能が必要
- 他のチームに情報が漏れるのはレギュレーション違反なので、最低限の認証は必要だと思います
- とはいえ漏れたら重大な問題が起きる、というほどのことではないので、BASIC認証(ユーザー名+パスワード)をつけておけば、マスターズ選手権の用途には十分なのかなと思います
- デプロイが素早くできる
- デプロイは可能な限り素早くできると嬉しいです
- コンテナをビルドするので、デプロイに数分かかる、みたいな事態は避けたいです
こう考えた時に良さそうなデプロイ先としてVercelがありました。
vercel.com
Vercelは
などの点でマスターズ選手権向けにはとても良さそうでした。
以下ではこの記事のテンプレートをvercelにデプロイして、BASIC認証をつけるために必要なことを書こうと思います。
とはいえ、vercelはすごく簡単で、Githubと連携してアカウントを作り、指示に沿って作業をするとフロントエンドのページをデプロイできてしまいます。
vercel自体の使い方についてはtutorial等を参考にしてください。
設定はこのようにするとうまくいくと思います:
Viteを選択して、ビルドはtsc & vite buildで実行してください。
以下ではテンプレートに対する変更に限って記述をしようと思います。
やるべきことは3つあり、
- wasmをビルドして、public/wasm以下のファイルをGitの管理下に置く
テンプレートではwasmのビルド結果はgitignoreでリポジトリから除外されているので、あらかじめビルドしてリポジトリの中に入れておく必要があります。
(やろうと思えばvercelでwasmのビルドができる気がするのですが、ローカルでやってしまうのが簡単だと思います)
yarn add @vercel/edge
コマンドを実行して、@vercel/edgeを追加しましょう。
これはBASIC認証をつけるために必要で、以下の記事を参考にしました。
zenn.dev
リポジトリのルートにmiddleware.jsを追加します(これも上記の記事を参考にしました)。
userとpasswordは適切に変更してください。
import { next } from '@vercel/edge';
export const config = {
matcher: '/',
};
export default function middleware(req) {
const authorizationHeader = req.headers.get('authorization');
if (authorizationHeader) {
const basicAuth = authorizationHeader.split(' ')[1];
const [user, password] = atob(basicAuth).toString().split(':');
if (user === 'ここを変更する!!!!' && password === 'ここを変更する!!!!') {
return next();
}
}
return new Response('Basic Auth required', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Secure Area"',
},
});
}
これはEdge Middlewareという技術を使っているそうです。
Edge Middleware is code that executes before a request is processed on a site
こう書いてあるので、ページの前にIDとパスワードを要求しているような処理を実行していることになります。
以上の3つの事項を実装すると、BASIC認証がついたページをデプロイすることができました。(この範囲だと無料プランでできるはず...)
最後に
マスターズ選手権が楽しみすぎてもう準備を始めてしまいました。
この記事はビジュアライザをどう書けばいいか悩んでいる方へのヒントになればいいかなと思います。
個人的にもRustとWebAssemblyを触る良い機会になったと思います。3月までにRustをたくさん練習しておきます。
(チームメンバーも探さなきゃ... 声をかけていただけると嬉しいです)
質問などがありましたらコメント欄やtwitterにご連絡いただければと思います。