こんにちは、株式会社Techouse バックエンドエンジニアの @nodematerial です。
今回は、RubyKaigi 2025 で開催された「TRICK」というイベントに参加した経験と、自分の作品について紹介させていただきます。
TRICKとは
TRICK (Transcendental Ruby Imbroglio Contest for rubyKaigi) は、Ruby の言語特性を活かした技巧的なプログラミングを競うコンテストです。参加者はRubyの特徴を活かした驚きや面白さを感じさせるコードを投稿し、数名の審査員が評価を行うという内容です。
今年のRubyKaigi で開催された TRICK 2025 は作品のレベルが非常に高い回だったようで、私の作品は入賞には至りませんでしたが、佳作という形で紹介いただきました。
私の応募作品
以下が応募した作品のコードです。
eval$s=%w(s=%(eval$s=%w(#{$s})*"");f=->n{s.sl ice!(0,n)};a=[7,116];b=[6,1,116,1];c=[0,1,128 ,1];d='oxVw';e='~}]y{y|{}}}}}}}}}}|}}}}}}}}}} {|y{yY}';g=['vA','u@','tA','syNy',*[[]]*7,'|3 ','{3','z4','ymRs','wwGy',*[[]]*4];h='~}^y|v| r|r|v|yZ}@~}_y|v}}|ypy|}}v|y[}@~}_z|v}{d{}v|z [}@~}`z{w}zyy}z}yyz}w{z\}@~}ay|w}zyy}x}yyz}w| y]}@~}az{w}z{x}v}x{z}w{z]}@~}azzx}|z|}}}}}t}} }}}|z|}xzz]}@~}azky}v}ykz]}@~}azzx}|vz}x}zv|} xzz]}@~}az{w}zv{}z}{vz}w{z]}@~}ay|w}zt|}|}|tz }w|y]}@~}`z{w}zszsz}w{z\}@~}_z|v}{r|r{}v|z[}@ ~}_y|v}}|ypy|}}v|y[}@~}^y|v|r|r|v|yZ}'.split( '@');i='~}{vp|m|z{`wxzxw|uz}@~}{|y{r|m|{{a|zz {{|{|{z{}|s}@~}{|y{r|m|||o|t|||y{|{z{}|y{}|}y y}@~}{vpxqyxwvxs{{{z{w{|{|zz}@~}{{{{z|||{|{|| |z|||||z|z|{|||{|}|yzy{z{zzt{z}@~}{{z{{|||{|{ |{|||{|{{||z|{|||{|}|{zv{|{{zx|z{z}@~}{{y{|w| yxzz|z{|y|}||{x}||tzx{t|wy}@~}Y|[|J}@~}Z|_|{| J}@~}[|\zI}@~}I{y|b{yyf}@~}p|i|||uzz|q|qzz|{| v|q}@~}qzq}t|s|zxy|||o|yyy|||q}@}}s|||zx|y||| |s|z|{|{z}y|yy|z|{|{z}yu}@|}uv{|{|}|y|||s|z|{ |z|||}|r|z|{|z|||}|v}@{}w|x||x||y|||ux{yy|||} |tx{yy|||}|w}@z}l|R}G}@y}m|R}H}'.split('@');j ='@x%vA%uA%tA%sykw%[w%]w%]x%^x%_xmz%lR%iS%awm u%bwlw%bwlx%cwkx%cwky%dwjy%ewiy%ewjx%fwix%gwh x%gwiv%|3%{3%z3%ytLs'.split('%');l1='picoairX @sbjigbvX@v]pan]xx@xrrqtxixrrrqzx@zslryxdxvsl r|x@{ufuyxdxwufu|x@|tfu{vcv{tfu|x@}ucv{vcv|uc v|xwk@}wav{vcv|wav|xze@}wbu{vcv|wbu|X@}wbu{vc v|wbu|mvo@}wct{vcv|wct|prp@\tzvcv[t{rnr@]tyvc v\t^r@`tvvcv_tZs@cstvcvbsXs@fsqvcvesUs@jsmvcv isQs@msjvcvlsNs@psgvcvosKs@rtdvcvqtHs@uu`vcvt uEr@wt]xcxttDr@zs[xcxws^qpq@{sXxgxvs[rro@}Tsa uSyZ@}SoiqSw^@}SlonSrf'.split('@');l2=[*g,'d` ','hY','jS','lu`v','mw\v','nwYw','owWw','owWw ',*[d]*5,'owWw','owWx','nwZw','mw\w','juet',' hW','c_',*[[]]*4,*g,*j];l3=[a,b,'y}OjK}','z}T o|nQ}','{}VujuR}','|}XwyvzvywT}','}}Yxzq|qzxU }','~}Zxyp|pyxV}','~}[y{||p|p||{yW}',e,*h,e,' ~}[y{||p|p||{yW}','~}Zxyp|pyxV}','~}Xxzq|qzxT }','~}VwyvzvywR}','~}SujuO}','~}Po|nM}','~}Jj F}',c,c,*i,b,a];l={'Number'=>l1,'Kanji'=>l2,' RubyKaigi'=>l3}[ARGV[0]];l.map{|l|l.is_a?(Str ing)?l.bytes.map{126-_1}:l}.each{|l|puts(l.ea ch_slice(2).map{|x,y|"\s"*x+f[y]}.join)};)*""
今回私が提出した作品は、3種類のASCIIアートを表示するプログラムです。このコードを entry.rb
として保存し、以下のようにコマンドを実行すると様々なASCIIアートが表示されます。
ruby entry.rb Number # 数字「2025」のASCIIアート ruby entry.rb Kanji # 漢字「二〇二五」の書道風ASCIIアート ruby entry.rb RubyKaigi # RubyKaigi 2025のロゴとイベント日程
出力結果は、全てQuine的な性質を持っており、出力されたASCIIアートをファイルに保存し、それを実行すると元のコードと全く同じ動作をします。例えば、以下のように実行できます。
ruby entry.rb Kanji > kanji.rb ruby kanji.rb Number > number.rb ruby number.rb RubyKaigi > rubykaigi.rb ruby rubykaigi.rb Kanji # 漢字「二〇二五」の書道風ASCIIアート
また、このコードは 45 × 45 の正方形として、2025文字で構成されています。2025年は89年ぶりの、平方数として表せる年であり、TRICKの仕掛けの一部として組み込みたかったのです。
TRICK 応募のきっかけ
きっかけは、東京Ruby会議12で「あなたの知らない超絶技巧プログラミングの世界」という本に出会ったことでした。自分は昨年のRubyKaigi にも参加していたので、TRICKの存在は認知していましたが、技術的な敷居の高さを感じて、参加することはないだろうと考えていました。
しかし、この本にはQuine(自分自身のソースコードを出力するプログラム)の作成方法や、豊富なサンプルコードが記載されており、本を読み進めるにつれ「自分にも超絶技巧プログラムが書けるかもしれない」という自信を得ることができました。
また、TRICK が数年に1度の開催であることもあり、これが自分にとって最後のチャンスなのではないかと考え、思い切って参加しようと考えました。
コード内部の仕組み
entry.rb
は一見複雑に見えますが、基本的には「レイアウト定義」と「出力部」の2つの部分に分かれています。
コード内では、3つの変数 l1
、l2
、l3
が定義されており、それぞれが以下のASCIIアートを表現する2次元配列に対応しています:
l1
: 数字(2025)のASCIIアートl2
: 漢字(二〇二五)のASCIIアートl3
: RubyKaigiロゴのASCIIアート
このASCIIアートを表現する2次元配列を「レイアウト定義」と呼ぶことにします。レイアウト定義は、ターミナル上で真心を込めて作成したアスキーアートを、Rubyプログラムで自動変換して生成しました。
そして出力部ではレイアウト定義通りに、スクリプトを出力します。出力もQuine である必要があるので、2025文字になるようレイアウト定義が調整されています。
困難だった点
最も難しかったのは、コード全体を2025文字に収めることでした。これを実現するために、以下に示すような、様々なコード圧縮技術を駆使しました。
1.ASCII エンコーディング
レイアウト定義には、2桁の数字が良く登場しました。ASCIIコードによる1byte文字と数字の対応づけを用いれば、2桁の数字を1byteで表現できるため、コードを圧縮することができます。
0〜127の範囲の文字は1バイトで表現できますが、制御文字(0~31) やスペース(32)、DEL(127)などはコードとして扱いにくいため、33〜126の表示可能な文字範囲を利用し、マッピング関数(126から数値を引く方法)を通じて0〜93の値を1バイトで表現しました。
レイアウト定義をASCIIコードで表す操作(Encode) は別のプログラムで実施し、ASCIIコードをレイアウト定義に戻す操作(Decode) は、entry.rb
内で実行されます。
# Encode data <= 93 ? (126-data).chr : data
# Decode data.is_a?(String)? 126-data.bytes[0] : data
2. 重複の排除
レイアウト定義には、しばしばパターンの重複が見られました。例えば、漢字「二〇二五」のASCIIアートは、漢字「二」が共通部分として複数回出現します。
このような共通部分を変数に割り当てて再利用することで、同じレイアウト定義を繰り返し記述する必要をなくし、コード長を短縮しました。
実際、コード内では以下のように記述されています。
a=[7,116] b=[6,1,116,1] c=[0,1,128,1] d='oxVw' # ASCIIエンコードされたレイアウト情報 e='~}]y{y|{}}}}}}}}}}|}}}}}}}}}}{|y{yY}'; g=['vA','u@','tA','syNy',*[[]]*7,'|3 ','{3','z4','ymRs','wwGy',*[[]]*4] # ASCIIエンコードされたレイアウト情報
3. String#split
の活用
レイアウト定義において二重配列を利用する際、通常の配列定義方法ではレイアウトに直接関係しない部分で多くの文字を必要とします。例えば、配列に 'a' という要素を含める場合、カンマで1バイト 引用符で2バイト、合計3バイトを本質的でない部分で消費することになります。
これは、2025文字という制約がある中で、非常に大きな負担となります。そこで Rubyの split
メソッドを活用した配列定義に置き換えることで、カンマの量を削減しました。
具体的には、a~k のアルファベットからなる配列を定義する場合、通常の定義方法 ['a','b','c','d','e','f','g','h','i','j','k']
では45文字必要ですが、'a@b@c@d@e@f@g@h@i@j@k'.split('@')
とすれば34文字で記述できます。
ChatGPTの活用
コードを短縮する過程では、ChatGPTが大いに役立ちました。 ChatGPT は、上記の String#split の活用方法や、レイアウト定義の繰り返し部分をさらに圧縮する方法を提案してくれました。
感想
TRICKへの挑戦を通じて、Rubyの言語仕様と文字列操作についての理解が深まりました。コードサイズの制約下で機能を実現することで、日常の開発では得られない視点と知識を得ることができました。
もし興味をお持ちでしたら、TRICK2025 のリポジトリ から入賞者のコードを実行してみてください。TRICKのような技巧的なプログラミングを通じて、Rubyの新たな魅力に気づけるかもしれません。
最後に、素晴らしいイベントを企画・運営されたRubyKaigiの皆様と審査員の方々に感謝いたします。このようなイベントや、Rubyコミュニティの存在が、Ruby エンジニアとしての成長を後押ししてくれていると感じています。
Techouseでは、社会課題の解決に一緒に取り組むエンジニアを募集しております。 ご応募お待ちしております。