競技プログラミング用 CLI & テストライブラリ contest.go の紹介

## はじめに

この記事は次のアドベントカレンダーの 18 日目の記事です。

- [Go 3 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/go3)
- ~~[競技プログラミング Advent Calendar 2020](https://qiita.com/advent-calendar/2020/competitive-programming)~~

~~当初 Go 3 カレンダーに投稿するつもりだった記事ですが、ふと競技プログラミングのカレンダーを見たらちょうど同じ 18 日目が空いておりましたので、両カレンダーにダブル投稿させていただくことにしました。~~ ダブル投稿できると思っていたら、実はできませんでした。すみません。うーむかっこ悪い…。

[contest.go]: https://github.com/yaegashi/contest.go
[golang]: https://golang.org
[AtCoder]: https://atcoder.jp
[Codeforces]: https://codeforces.com
[Google Code Jam]: https://codingcompetitions.withgoogle.com/codejam
[diligence.go]: https://github.com/yaegashi/diligence.go

## contest.go

[contest.go] は、 [Go 言語][golang]で競技プログラミングコンテストに出場することを支援する目的で作った CLI とテストライブラリです。次のような特徴があります。

- 解答プログラムが標準入出力を使用するコンテストに対応 ([AtCoder], [Codeforces], [Google Code Jam] など)
- go test で自分の解答のテストができる (制限あり)
- AtCoder 対応
  - 過去のコンテスト、開催中のコンテストの問題のテストケースを自動ダウンロードする機能
  - 解答を提出する機能はまだない

実際に contest.go を使ってみたいという方のために、チュートリアルの形で使用法を説明していきたいと思います。

## チュートリアル

### インストール

contest.go を使うには、まず Go の開発環境が必要になります。 Go で競技プログラミングコンテストに出場するのですから、当然用意していますよね？

ということで、最初に次のコマンドで CLI コマンドである contest-cli をインストールしてください。

```console
$ go get github.com/yaegashi/contest.go/cmd/contest-cli
```

### 解答テンプレートとテストケースの生成

次に空の Git リポジトリないしフォルダを作り、その中で適当な名前の Go モジュールを作ってください。

```console
$ git init solutions
Initialized empty Git repository in /home/yaegashi/solutions/.git/
$ cd solutions/
$ go mod init solutions
go: creating new go.mod: module solutions
```

conetst-cli new により、一般的な解答フォルダを作成できます。 contest.go ではこのように 1 プログラムごとに 1 フォルダを使用します。

```
$ contest-cli new foo
2020/12/18 20:54:12 I: Created foo/main.go
2020/12/18 20:54:12 I: Created foo/main_test.go
2020/12/18 20:54:12 I: Created foo/sample1.in.txt
2020/12/18 20:54:12 I: Created foo/sample1.out.txt
```

フォルダ内には contest-cli 内蔵のテンプレートから簡単な解答プログラムとテストプログラム、正解になるテストケースが生成されます。 go test を実行して確認してみましょう。

```console
$ go test -v ./foo
go: finding module for package github.com/yaegashi/contest.go/tester
go: found github.com/yaegashi/contest.go/tester in github.com/yaegashi/contest.go v0.0.4
=== RUN   TestContest
=== RUN   TestContest/0:sample1.in.txt
--- PASS: TestContest (0.00s)
    --- PASS: TestContest/0:sample1.in.txt (0.00s)
PASS
ok      solutions/foo   0.291s
```

このように正解の解答プログラムではテストが成功します。

### AtCoder テストケースのダウンロード

次に AtCoder の過去のコンテスト問題のテストケースを contest-cli atcoder new で取り寄せてみましょう。

```console
$ contest-cli atcoder new abc069
2020/12/18 20:56:55 I: Fetching AtCoder contest abc069
2020/12/18 20:56:55 I: Created abc069/a/main.go
2020/12/18 20:56:55 I: Created abc069/a/main_test.go
2020/12/18 20:56:55 I: Created abc069/a/sample1.in.txt
2020/12/18 20:56:55 I: Created abc069/a/sample1.out.txt
2020/12/18 20:56:55 I: Created abc069/a/sample2.in.txt
2020/12/18 20:56:55 I: Created abc069/a/sample2.out.txt
2020/12/18 20:56:55 I: Created abc069/b/main.go
2020/12/18 20:56:55 I: Created abc069/b/main_test.go
2020/12/18 20:56:55 I: Created abc069/b/sample1.in.txt
2020/12/18 20:56:55 I: Created abc069/b/sample1.out.txt
2020/12/18 20:56:55 I: Created abc069/b/sample2.in.txt
2020/12/18 20:56:55 I: Created abc069/b/sample2.out.txt
2020/12/18 20:56:55 I: Created abc069/b/sample3.in.txt
2020/12/18 20:56:55 I: Created abc069/b/sample3.out.txt
2020/12/18 20:56:55 I: Created abc069/c/main.go
2020/12/18 20:56:55 I: Created abc069/c/main_test.go
2020/12/18 20:56:55 I: Created abc069/c/sample1.in.txt
2020/12/18 20:56:55 I: Created abc069/c/sample1.out.txt
2020/12/18 20:56:55 I: Created abc069/c/sample2.in.txt
2020/12/18 20:56:55 I: Created abc069/c/sample2.out.txt
2020/12/18 20:56:55 I: Created abc069/c/sample3.in.txt
2020/12/18 20:56:55 I: Created abc069/c/sample3.out.txt
2020/12/18 20:56:55 I: Created abc069/c/sample4.in.txt
2020/12/18 20:56:55 I: Created abc069/c/sample4.out.txt
2020/12/18 20:56:55 I: Created abc069/c/sample5.in.txt
2020/12/18 20:56:55 I: Created abc069/c/sample5.out.txt
2020/12/18 20:56:55 I: Created abc069/d/main.go
2020/12/18 20:56:55 I: Created abc069/d/main_test.go
2020/12/18 20:56:55 I: Created abc069/d/sample1.in.txt
2020/12/18 20:56:55 I: Created abc069/d/sample1.out.txt
2020/12/18 20:56:55 I: Created abc069/d/sample2.in.txt
2020/12/18 20:56:55 I: Created abc069/d/sample2.out.txt
2020/12/18 20:56:55 I: Created abc069/d/sample3.in.txt
2020/12/18 20:56:55 I: Created abc069/d/sample3.out.txt
```

上記の例では [AtCoder Beginner Contest 069](https://atcoder.jp/contests/abc069) に含まれる 4 つの問題の解答フォルダを作成しています。それぞれのフォルダに、解答のテンプレートとテスト実行のコード、そして AtCoder サイトからダウンロードされたテストケースが格納されています。

今回は解答テンプレートコードのままでは当然ながら不正解になるはずです。問題 A でテストしてみましょう。

```console
$ go test -v ./abc069/a
=== RUN   TestContest
=== RUN   TestContest/0:sample1.in.txt
    tester.go:107: Wrong answer:
        --- want
        +++ got
        @@ -1,1 +0,0 @@
        -6
        @@ -0,0 +1,1 @@
        +hello, 3
=== RUN   TestContest/1:sample2.in.txt
    tester.go:107: Wrong answer:
        --- want
        +++ got
        @@ -1,1 +0,0 @@
        -1
        @@ -0,0 +1,1 @@
        +hello, 2
--- FAIL: TestContest (0.00s)
    --- FAIL: TestContest/0:sample1.in.txt (0.00s)
    --- FAIL: TestContest/1:sample2.in.txt (0.00s)
FAIL
FAIL    solutions/abc069/a      0.277s
FAIL
```

このように、不正解の場合はテストが失敗します。また正答とのプログラムの出力の差分を表示してくれます。差分は、ただのテキストだとよくわかりませんが、 実際の端末ではカラフルに表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/cb47f589-86c4-a8e6-5726-f418847422d7.png)

### 解答プログラムの作成

[ABC069 の A 問題](https://atcoder.jp/contests/abc069/tasks/abc069_a)の解答プログラムを作成してみましょう。エディタでテンプレート生成された abc069/a/main.go を開きます。解答を記述する場所は、ファイルの最後にある `contest` 型の `main()` メソッドです。

```go
func (con *contest) main() error {
	var s string
	con.Scan(&s)
	con.Println("hello,", s)
	return nil
}
```

go test を使用する都合上、解答プログラムの入出力は `fmt.Scan()` や `fmt.Println()` といった関数の代わりに `con.Scan()` や `con.Println()` を使います。基本的には `fmt` を `con` に置き換えて書けばよいです。

AtCoder のようなコンテストでは解答プログラムは 1 ファイルで提出する必要があるため、利用可能なメソッドは main.go の中にすべて定義してあります。

さて、この問題の解答としては次のようなプログラムを書けばよいでしょう。

```go
func (con *contest) main() error {
	var n, m int
	con.Scan(&n, &m)
	con.Println((n - 1) * (m - 1))
	return nil
}
```

正しいプログラムが書けたら、再度 go test を実行してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/ce7193c6-5060-d357-276e-43cf35214cc0.png)

このように go test が成功したらテストケースでの確認はできたことになりますが、それが正答とは限らないので、提出する前によく見直しましょう。

また contest.go が対応できないタイプの問題もあります。詳しくは後述する制限事項も参照してください。

### AtCoder への提出

main.go ファイル全体が解答プログラムとなりますので、これをそのまま AtCoder に提出します。

ただし contest-cli にはまだ解答プログラムを提出する機能がないので、お手数ですが Web ブラウザを使って提出してください。

コンテストの解答プログラムは大した行数にはならないはずなので、エディタで main.go を全選択してからブラウザのフォームへのコピー・ペーストで実用上は問題ないと思います。

### AtCoder ログイン

このチュートリアルでは AtCoder の過去コンテンストのテストケースをダウンロードしましたが、開催中のコンテストのテストケースをダウンロードすることもできます。そのためには contest-cli atcoder login でログインしておきます。

```console
$ contest-cli atcoder login
AtCoder Login: yaegashi
Password: 
```

このようにログイン名とパスワードを入力してなにも表示されなければログインは成功しています。ログインのクッキーは ~/.contest-cli/atcoder.json に保存されています。

## 制限事項

現在の contest.go バージョン v0.0.4 には、次のような制約があります。

- テストの判定では、テストケース出力に対する単純な行単位の文字列マッチングしか行わないため、次のような問題では正しく判定ができません。
  - 正答が 1 通りだけではない問題 (例: [ABC069 問題 D](https://atcoder.jp/contests/abc069/tasks/arc080_b))
  - 浮動小数点の数を出力する問題
  - インタラクティブ問題
- 生成する解答テンプレートのカスタマイズはできません。コンテストに出場するなら Go の開発環境はあると思いますので、 [template.go](https://github.com/yaegashi/contest.go/blob/master/tester/templates.go) の文字列定数を編集して自分でビルドしてください。

## 今後の計画

- AtCoder 以外のコンテストの対応: Codeforces などにも対応したいです。
- AtCoder 解答提出対応: ログインしてテストケースをダウンロードする処理まで作ったのですから、あと一歩で実現できる機能だと思いますが、 web ブラウザにコピペ提出で十分早いのでまだ手をつけずにいます。
- テスト判定の改善: 前述の制限事項を解消するため改善が必要だと思っています。カスタムジャッジが書けるとよいのですが、実際のコンテンストではそんなのを書いている余裕はないため、必要ない機能かもしれません。
- テンプレートのカスタマイズ: ~/.contest-cli フォルダにテンプレートファイルを置くだけの簡単なもので十分かもしれません。

## おわりに

きっと同じような目的でもっと便利なソフトがすでに存在すると思うのですが、よく調査しておりませんでした。申しわけありません。

自分がこれまでのコンテストで Go 言語で作った解答プログラムやライブラリは [diligence.go] というリポジトリに記録していますので、よろしければ参考にしてください。