対戦型Quine Snake Battleをruby 2.7に対応させてCIを追加した

f:id:h6n:20191201035052p:plain

この記事はOkinawa.rb Advent Calendar 2019の1日目の記事です。 明日書く人を募集しています。

2.7対応

最新の2.7に対応しました

Before

こちらが対応前の2.7向けのQuineです。今はなき .: を使っているのがわかりますね。

eval($c=%w(require'socket';$c="eval($c=%w(#{$c}).join)";[[436,6],[515,10],[593,5
],[602,4],[672,4],[679,1],[681,1],[683,4],[752,4],[760,2],[763,5],[831,3],[840,1
],[842,7],[911,7],[919,1],[921,8],[991,7],[999,1],[1005,4],[1072,7],[1085,4],[11
53,15],[1235,11]].each{$c[_1,0]=0x20.chr*_2};;;;;;;;;;;;;;;;;;;H,W,T,A=24,80,Thr
ead,Array;using(Module.new{refine(A){define_method(:y){first};define_method(:x){
last}}});m,ts,fs=nil,[],[];print("\x      1b[2J");dm,bp,rt={"\e[A"=>->y,x{[(y-1)
%H,x]},"\e[B"=>->y,x{[(y+1)%H,x]},"          \e[C"=>->y,x{[y,(x+1)%W]},"\e[D"=>-
>y,x{[y,(x-1)%W]}},->{loop{;;;;;"     ____    ";x=rand(W);y=rand(H);unless(m[y][
x].start_with?("\x1b"));return("    _/. . L    ";[y,x]);end}},T.start{loop{nm=A.
new(H){A.new(W){''}};i=0;(;;;;;"    L___  /     ";nm).each_with_index{|r,y|r.eac
h_with_index{|c,x|nm[y][x]=$c[(   %>~~L/ /       ">;i)]||'#';i+=1}};_=ts.select{
|t|t.respond_to?(:n)}.each{|t|"       | /        ";t.n;t.b.each{|y,x|;;nm[y][x]=
"\x1b[48;5;%dm%s\x1b[0m"%[(;;;"       | |__/|    ";t.p),nm[y][x]]}};m=nm;_.combi
nation(2){_2.b.include?(_1.b[(;"       L____/    ";0)])&&_1.e;_1.b.include?(_2.b
.first)&&_2.e};fs.size<5&&fs<<bp.               ();fs.each{m[_1][_2]="\x1b[48;5;
1m%s\x1b[0m"%m[_1][_2]};$stdout.pri           nt(m.map.with_index{|r,y|"\x1b[#{y
+1};1H\x1b[K"+r.join}.join);sleep(0.1)}};p=1;gs=TCPServer.open(1234);begin;loop{
ts<<T.start(gs.accept){|s|q,d,l,b=(p=p.next),"",true,[bp.()];f=T.current.:define
_singleton_method;f.(:p){q};f.(:d){d};f.(:e){l=false};f.(:b){b};f.(:n){nb=b.dup;
h=nb.first;e=nb.pop;nh=dm[d]&.(*h)||h;nb.unshift(nh);fs.delete(nh)&&nb.push(e);b
=nb;};s.print([255,253,34,255,250,34,1,0,255,240,255,251,1].pack('c*')+"\x1b[2J"
+"\x1b[?12l");loop{begin;d=s.read_nonblock(3);rescue(IO::EAGAINWaitReadable);res
cue;break;end;s.print(m.map.with_index{|r,y|"\x1b[#{y+1};1H\x1b[K"+r.join}.join)
;l||break}rescue(1);s.close;ts.delete(T.current)}};rescue(Interrupt);end).join)#

After

こちらが対応後の2.7向けのQuineです。 .:method メソッドで置き換えました。

eval($c=%w(require'socket';$c="eval($c=%w(#{$c}).join);";[[436,6],[515,10],[593,
5],[602,4],[672,4],[679,1],[681,1],[683,4],[752,4],[760,2],[763,5],[831,3],[840,
1],[842,7],[911,7],[919,1],[921,8],[991,7],[999,1],[1005,4],[1072,7],[1085,4],[1
153,15],[1235,11]].each{$c[_1,0]=0x20.chr*_2};;;;;;;;;;H,W,T,A=24,80,Thread,Arra
y;using(Module.new{refine(A){define_method(:y){first};define_method(:x){last}}})
;m,ts,fs=nil,[],[];print("\x1b[2J");      dm,bp,rt={"\e[A"=>->y,x{[(y-1)%H,x]},"
\e[B"=>->y,x{[(y+1)%H,x]},"\e[C"=>-          >y,x{[y,(x+1)%W]},"\e[D"=>->y,x{[y,
(x-1)%W]}},->{loop{x,=rand(W);;;"     ____    ";y=rand(H);unless(m[y][x].start_w
ith?("\x1b"));return[y,x]end}};"    _/. . L    ";T.start{loop{nm=A.new(H){A.new(
W){''}};i=0;nm.each_with_index{"    L___  /     ";y=_2;_1.each_with_index{|c,x|n
m[y][x]=$c[i]||'#';i+=1}};;;;;;   %>~~L/ /       ">;_=ts.select{|t|t.respond_to?
(:n)}.each{|t|t.n;t.b.each{|y="       | /        ",x|;;;nm[y][x]="\x1b[48;5;%dm%
s\x1b[0m"%[t.p,nm[y][x]]}};;;;"       | |__/|    ";m=nm;_.combination(2){_2.b.in
clude?(_1.b[0])&&_1.e;b=_1.b;;;"       L____/    ";b.include?(_2.b.first)&&_2.e}
;fs.size<5&&fs<<bp.();fs.each{m[_               1][_2]="\x1b[48;5;1m%s\x1b[0m"%m
[_1][_2]};$stdout.print(m.map.with_           index{|r,y|"\x1b[#{y+1};1H\x1b[K"+
r.join}.join);sleep(0.1)}};p=1;gs=TCPServer.open(1234);begin;loop{ts<<T.start(gs
.accept){|s|q,d,l,b=(p=p.next),"",true,[bp.()];f=T.current.method(:define_single
ton_method);f.(:p){q};f.(:d){d};f.(:e){l=false};f.(:b){b};f.(:n){nb=b.dup;h=nb.f
irst;e=nb.pop;nh=dm[d]&.(*h)||h;nb.unshift(nh);fs.delete(nh)&&nb.push(e);b=nb;};
s.print([255,253,34,255,250,34,1,0,255,240,255,251,1].pack('c*')+"\x1b[2J"+"\x1b
[?12l");loop{begin;d=s.read_nonblock(3);rescue(IO::EAGAINWaitReadable);rescue;br
eak;end;s.print(m.map.with_index{|r,y|"\x1b[#{y+1};1H\x1b[K"+r.join}.join);l||br
eak}rescue(1);s.close;ts.delete(T.current)}};rescue(Interrupt);end).join);######

対戦型QuineのCI

対戦型Quineを書いている皆さんはCIでお困りではないでしょうか?

対戦型なので実際対戦してみないと動くかどうかはわかりません。 しかし対戦するスクリプトを書いてテストするのは手間です。 でもテストを書いてCIしないと安定してQuineを届けられないですよね...困った。

今回は、対戦型Quineに簡易的に文法チェックをするだけのCIを追加してみました。

スクリプトが文法的に正しいかどうかは ruby -c でテストできます。 ただしスクリプトのほとんどの部分が eval に渡す文字列の中で書かれているのでそれだけではあまり意味がありません。 eval に渡しているスクリプトの文法のテストは eval をstubして RubyVM::AbstractSyntaxTree.parse(src) してやるとパース可能かどうかがわかります。

GitHub Actionsでの設定は以下のような感じです、べんりですね! ご自由にお使い下さい。

name: Syntax OK

on:
  - push

jobs:
  syntax_ok:
    name: Syntax OK
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        ruby:
          - ruby-head
          - ruby-2.7
    steps:
      - uses: actions/checkout@master
      - name: Set up RVM
        run: curl -sSL https://get.rvm.io | bash -s -- master
      - name: Set up Ruby
        run: |
          source $HOME/.rvm/scripts/rvm
          rvm install ${{ matrix.ruby }} --binary
          rvm --default use ${{ matrix.ruby }}
      - name: Test Syntax OK
        run: |
          source $HOME/.rvm/scripts/rvm
          ruby -c snake.rb
      - name: Test Syntax OK (eval)
        run: |
          source $HOME/.rvm/scripts/rvm
          ruby -e 'def eval(src); RubyVM::AbstractSyntaxTree.parse(src) ;end; load "snake.rb"'

まとめ

QuineをCIする手法を2つ紹介しました。

  • ruby -c で文法チェック
  • eval をstubしてevalするコードを RubyVM::AbstractSyntaxTree.parse で文法チェック

いかがでしたか? お手持ちのQuineでぜひ試してみて下さい✨💎✨

余談: GitHub Actionsハマりどころ

matrixでテストを組むときに fail-fast: false しておかないと一見うまく行っているように見えるjobが他のjobが落ちたタイミングで ##[error]The operation was canceled. と出て止まってしまいどうしたらいいのか分からなくて小一時間ぐらい(実際は2時間以上)つかってしまった。

jobs:
  syntax_ok:
    strategy:
      matrix:
        ruby:
          - ruby-head
          - ruby-2.7
      fail-fast: false

いいなと思ったらKyashでお金を下さい
20191128011151
GitHubスポンサーも受け付けています
https://github.com/sponsors/hanachin/