freeeの開発情報ポータルサイト

『勝手にコードゴルフ王決定戦 in RubyKaigi 2024』を開催しました! - 前編

どうも、freee 沖縄拠点のエンジニア yossi です。 RubyKaigi 2024、めっちゃ盛り上がりましたね。楽しかった…!!

特に今回は沖縄開催!ということで、普段はなかなか東京のイベントに絡めない我々沖縄拠点のエンジニアも、ここぞとばかりにブースを盛り上げるべく参戦しました!

今回プラチナスポンサーとしてブース出展するにあたって「勘定科目クイズ」や「Ruby アンケート」などいくつかの展示をやっていたのですが、このエントリーでは我々が出題していたコードゴルフの問題について語ろうと思います。

なんとこのコードゴルフのプログラム、実行するとこんなふうに沖縄の地図がコンソール上に出力されるんです!

完成したコードゴルフ問題の出力。沖縄の地図だ!
完成したコードゴルフ問題の出力。沖縄の地図だ!

よかったら clone して手元で実行してみてくださいね!

コードゴルフの問題をGitHubで見てみる!

その前に、コードゴルフって何?

お題に沿ったプログラムを、「いかに少ない byte 数のコードで動作させるか」を競う協議です。 見た目がどんなにぐちゃぐちゃに見えてもとにかく要件を満たして動けばそれでいいんですよね。

特に Ruby は色んな省略記法やイディオムが存在し、コードの byte 数をぎゅぎゅっと圧縮するテクニックが数多くあります。 ですので、コードゴルフにおいては出題者が予想もしないような回答が期待できる言語といえるでしょう。

企画会議

テッキーなプログラマ向けの企画も欲しいよねということで、企画会議で「コードゴルフをやろう」という話になりました。 初期の企画ミーティングでは、例えば以下のような案が出ていました。

  • 沖縄に特有の名字の文字列をなんかゴニョゴニョする問題
  • シーサーのアスキーアートを出力する問題

ただ、もっともっと面白い問題にするために何か手伝えないかな?と思ったのがきっかけで、自分でもアイデア出しを始めたのがこの話のスタートとなります。

良いコードゴルフの問題ってどんなんよ?

yossi が考えるに、良いコードゴルフの問題の条件は以下のようなものを満たすものです。

  • 単純に、動作させた際に視覚的に面白い挙動となること
  • コード圧縮のための創意工夫を掻き立てられる
  • 元のスクリプトは 100 byte からせいぜい 300 byte 程度の、軽量なものである

次に、Ruby の特性を利用するとき、どのような問題だったらより面白くなりそうでしょうか? yossi が考えたのは以下のようなものです。

  • 配列や文字列の操作がある
  • コンソール上ですぐに実行結果が確認できる
  • 想定回答を考えたとき、ruby に特有のメソッドが有効に活用できる
    • 数値.times
    • 配列.product など

上記の考えをベースとして、よりアイデアを具体化していくことにします。

アイデア出し

せっかく沖縄で開催されるので、何かしら沖縄にちなんだ問題にするのが良さそうです。 いろいろとメモに書き溜めたりしながら、実はここで2日ぐらい悩んだりしていました。

最終的には、企画段階で出ていた『沖縄に特有の名字』というのがヒントとなり、「沖縄の地名を用いる問題がなんだか面白そうだぞ」とアイデアが湧いてきました。 そこで「沖縄 地図 抽象的」でググったりしていると、沖縄県の形が市町村の漢字だけで表現された地図を発見しました。

これだ!と思ったわたしは、早速問題を実装するべく ChatGPT を立ち上げました。

大まかな仕様を考える

沖縄の地図をコンソール上で出力する!というお題に沿って、要件をまとめていきます。

  • 1つの市町村は、2×2の正方形として出力される
  • 市町村の区切りが分かるように、背景を色塗りする
  • input となる json に、市区町村名、出力される位置を示す座標をセットで埋め込む

うん、なんとなく行けそうな気がしてきました! ここまでくれば、あとはヤツの出番です。

GPT よ、あとは頼んだぞ

モデルに GPT4 を選び、仕様について何度か壁打ちしました。

最終的に、以下のようなリクエストを流しました。

GPT への入力

ruby のイベントで使うコードゴルフの問題を考えています。
沖縄で開催されるイベントであるため、コンソール上に沖縄県の概略地図を出力するスクリプトを作成したいです。

## 仕様

- 一つの市区町村は2✕2の正方形で表す
- 正方形の中に市区町村の名称が出力される
- 出力例: 那覇市の場合

---
那覇
市_
---

- 出力例: 今帰仁村の場合

---
今帰
仁村
---

- ソースコード上で、各市区町村と正方形が出力されるべき位置の対応関係をマッピングした配列を用意します。
  - なお、出力位置を表す coordinate は、一番左上が `[0, 0]` とします。

---
mappings = [
  {
    name: '那覇市',
    coordinate: [0, 7] # 右に0マス、下に14マス,
    ansi_color_code: 'e[30m'
  }, 
  {
    name: '今帰仁村',
    coordinate: [5, 1] # 右に10マス、下に2マス,
    ansi_color_code: 'e[41m'
  }, 
  ...
]
---

- 各市町村の文字の背景は ANSI コードを利用して色塗りされる
- 陸地がない海のマスは青背景の全角スペース ` ` が出力される
- 地図が出力される範囲は、縦が20マス、横が16マス とする

## スクリプトの方針

### ステップ1: 

まず、出力される文字を全て配列で表現し、初期化します

---
map_text_array = [
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],  # 横16列分
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], 
  [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}], 
  # ... 縦20行分
]
---

### ステップ2: 

mappings を順に処理します。

- まず、`那覇市` は3文字なので、`那覇市 ` という全角スペースを含む4文字の文字列に修正します。
- coordinate が [0, 7] なので、以下のように文字とカラーをマッピングします。

1文字目の`那`: 行は  7 * 2 + 1 = 15 行目、列は 0 * 2 + 1 = 1 列目
よって、maapings[15-1][1-1] = {text: '那', color: 'e[30m'}

2文字目の`覇`: 行は  7 * 2 + 1 = 15 行目、列は 0 * 2 + 2 = 2 列目
よって、maapings[15-1][2-1] = {text: '覇', color: 'e[30m'}

3文字目の`市`: 行は  7 * 2 + 2 = 16 行目、列は 0 * 2 + 1 = 1 列目
よって、maapings[16-1][1-1] = {text: '市', color: 'e[30m'}

4文字目の` `: 行は  7 * 2 + 2 = 16 行目、列は 0 * 2 + 2 = 2 列目
よって、maapings[16-1][2-1] = {text: ' ', color: 'e[30m'}

mappings に4文字目まで上書きしたら、次の市町村にループします

### ステップ3:

mappings で上書きされなかったものは、text を全角スペース、color を青で埋めます

### ステップ4:

mappings の1次元目を改行コードで cat し、ansi コードを含む文字列として出力します。

## 命令

上記の仕様に基づいて、地図を出力する ruby スクリプトを出力してください。
なお、mappings 配列は私が提供したサンプルのままとし、それ以外の関数について出力してください。

入力のポイント

考えていた要件を満たすような出力を得るため、以下のようなことを考えて入力を行います。

  • 入力の具体例を一部でも良いので書く
  • 仕様、スクリプトの方針、命令について分けて入力する
  • スクリプトの方針については、ステップに分けて指示を行う

GPT からの出力

入力に対し、以下のような出力がありました。

以下は、沖縄県の地図を出力するRubyスクリプトのサンプルです。このスクリプトは指定された仕様に従って、市町村の配置と色をコンソールに表示します。
このコードには、市区町村のマッピングとその他の関数が含まれており、mappings配列はサンプルのまま使用します。

# ANSIカラーコードの設定
ANSI_BLUE = "\e[44m"
ANSI_RESET = "\e[0m"
# 沖縄県の市町村配置
mappings = [
  {
    name: '那覇市',
    coordinate: [0, 7],
    ansi_color_code: "\e[30m"
  }, 
  {
    name: '今帰仁村',
    coordinate: [5, 1],
    ansi_color_code: "\e[41m"
  },
  # 他の市町村のデータもここに追加
]
# 地図の初期化
map_text_array = Array.new(20) { Array.new(16) { { text: ' ', color: ANSI_BLUE } } }
# mappingsから地図情報を設定
mappings.each do |mapping|
  name = mapping[:name] + ' '
  row = mapping[:coordinate][1] * 2
  col = mapping[:coordinate][0] * 2
  color = mapping[:ansi_color_code]
  # 文字列を2x2のブロックに配置
  map_text_array[row][col]     = { text: name[0], color: color }
  map_text_array[row][col + 1] = { text: name[1], color: color }
  map_text_array[row + 1][col] = { text: name[2], color: color }
  map_text_array[row + 1][col + 1] = { text: name[3], color: color }
end
# 地図をANSIコード付きで出力
map_text_array.each do |row|
  puts row.map { |cell| "#{cell[:color]}#{cell[:text]}#{ANSI_RESET}" }.join
end

さて、半信半疑ながらも、冒頭の mappings 配列をある程度自分で埋めたうえで上記を試しに実行してみると…!!

中部が水没したような、コンソール上の沖縄県の地図
中部が水没したような、コンソール上の沖縄県の地図

…動いてる!なんかできてそう!!!

競技用にブラッシュアップ

さて、このままではまだがんばって回答を短くしても X 上では文字数上限に引っかかって回答できなさそうということで、 yongi, otyamura, fumi が問題をブラッシュアップしてくれました。

  • 市区町村の配列は、協議プログラミングにおける標準入出力のようにして別ファイルに切り出す
  • 入力部と出力部は固定とし、二次元配列を整形する部分のみを問題とする
  • リポジトリとして整備する

こうしてできたのが、冒頭でもあった以下のリポジトリです。

https://github.com/freee/rubykaigi2024-codegolf

配布用チラシも作ったんじゃ

やはり地図として出力されている様子が面白いので、チラシを作りました。

コードゴルフの問題を説明するチラシ
コードゴルフの問題を説明するチラシ

チラシを指さして、「ほら、こんなふうにコンソールに地図が出力されるんですよ」というと、足を止めて見てくださる方が多かったように思います。 ひと目でそれが分かるのと、QR コードで URL に簡単にアクセスできるようになるので、これは作っておいて良かった〜!

感謝

  • なんといっても問題をブラッシュアップしてくれた yongi, otyamura3, fumi3!ここからもう一捻り、骨が折れますよね。そして、リポジトリ整備まで。本当にありがとう…!!
  • 地図の形や市町村の読み仮名を監督してくれた intel 。これが無いと「沖縄の地図です」と言っても説得力が出なかったところです。感謝!
  • 集計をやってくれた otyamura3、これも開催期間の合間を縫ってやってくれて、なかなかに大変だったと思います。お疲れ様でした!
  • 最終日、臨時で現在記録を掲載して通行人や X のフォロワーを煽ってくれた @kemuridama さん。これで最終的に、本当に 1 byte を争いが繰り広げられました。あざます!

後編へ続く?

回答は X 上で募集していたのですが、Ruby コミッターの参戦もあったりして、我々の想像を超えた大混戦となりました! 詳細や回答の解説は、yongi が書いてくれた後編の記事を読んでみてください!

〜 次回、1 byte を争う白熱の回答合戦!? - 後編へ続く 〜