Quineスネークバトル🐍

おもしろかったらStar https://github.com/hanachin/quine-snake

おもしろかったら💖して 🔁

複数プレイヤーいるとこういう感じ

f:id:h6n:20190807000935p:plain

モチベーション

Quineの猛者とQuineで戦うためには対戦型のQuineを書いてQuineで戦う(?)しかない。

ソースコード

これをruby 2.7+で実行すると1234ポートでサーバーが立ち上がります。プレイヤーはtelnetしてジョインし、戦います。

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)#

プレイヤーの画面にもサーバーの画面にもこのソースコードが表示されるのでコピペするだけで再実行できる。対戦型Quineは再配布にもべんり。

解説

比較的読みやすい(ほぼ)等価なコードはこちら。

require 'socket'
gs = TCPServer.open(1234)
addr = gs.addr
addr.shift
puts('server is on %s' % addr.join(':'))

# ここ参考に書いた
# https://stackoverflow.com/questions/4532344/send-data-over-telnet-without-pressing-enter/4532379
#
# 関連するtelnetのrfc
# https://tools.ietf.org/html/rfc854
# https://tools.ietf.org/html/rfc1184
# https://tools.ietf.org/html/rfc857
SB       = 250 # sequence beign
SE       = 240 # sequence end
WILL     = 251
DO       = 253
IAC      = 255
LINEMODE = 34
LM_MODE  = 1
ECHO     = 1

IAC_DO_LINEMODE = [IAC, DO, LINEMODE].pack('c*').freeze
IAC_SB_LINEMODE_0_IAC_SE = [IAC, SB, LINEMODE, LM_MODE, 0, IAC, SE].pack('c*').freeze
IAC_WILL_ECHO = [IAC, WILL, ECHO].pack('c*').freeze

SKIP_SIZE = 1000

CSI = "\x1b["
ERASE_ALL = '2'

CURSOR_UP="\e[A"
CURSOR_DOWN="\e[B"
CURSOR_RIGHT="\e[C"
CURSOR_LEFT="\e[D"

HEIGHT = 24
WIDTH  = 80

using Module.new {
  refine(Array) {
    def y; first; end
    def x; last; end
  }
}

def init_map
  Array.new(HEIGHT) { Array.new(WIDTH) { false } }
end

threads = []

def new_body_from(feeds, thread)
  new_body = thread.body.dup
  head = new_body.first
  tail = new_body.pop
  new_head =
    case thread.direction
    when CURSOR_UP
      [(head.y - 1) % HEIGHT, head.x]
    when CURSOR_DOWN
      [(head.y + 1) % HEIGHT, head.x]
    when CURSOR_RIGHT
      [head.y, (head.x + 1) % WIDTH]
    when CURSOR_LEFT
      [head.y, (head.x - 1) % WIDTH]
    else
      head
    end
  new_body.unshift(new_head)

  if feeds.delete([new_head.y, new_head.x])
    new_body.push(tail)
  end

  new_body
end


feeds = []
render_thread = Thread.start do
  map = init_map

  Thread.current.define_singleton_method(:map) { map }
  Thread.current.define_singleton_method(:blank_point) {
    loop {
      y = rand(1..HEIGHT)
      x = rand(1..WIDTH)

      break [y, x] unless map[y][x]
    }
  }

  loop do
    new_map = init_map
    threads.each do |t|
      t.body = new_body_from(feeds, t)
      t.body.each do |y,x|
        new_map[y][x] = t.player
      end
    end
    map = new_map

    threads.each do |t|
      threads.each do |t2|
        next if t == t2
        t.dead if t2.body.include?(t.body.first)
      end
    end

    if feeds.count < 3
      feeds << Thread.current.blank_point
    end

    feeds.each { |f| map[f.y][f.x] = '@' }

    sleep 0.1
  end
end


using Module.new {
  refine(TCPSocket) {
    def erase_all
      print CSI, ERASE_ALL, 'J'
    end

    def move_cursor_lefttop
      move_cursor(1, 1)
    end

    def erase_line
      print CSI, "K"
    end

    def move_cursor(row, column)
      print CSI, '%d;%dH' % [row, column]
    end

    define_method(:render) {
      move_cursor_lefttop
      render_thread.map.each.with_index(1) { |row,y|
        move_cursor(y, 1)
        erase_line
        print(row.map { |x| x ? x : ' ' }.join +  "\n")
      }
    }
  }
}

player = "A"

loop {
  threads << Thread.start(gs.accept) { |s|
    new_player = player
    player = player.next
    Thread.current.define_singleton_method(:player) { new_player }
    Thread.current.define_singleton_method(:s) { s }
    direction = CURSOR_RIGHT
    Thread.current.define_singleton_method(:direction) { direction }

    live = true
    Thread.current.define_singleton_method(:dead) { live = false }
    Thread.current.define_singleton_method(:live) { live }

    body = [render_thread.blank_point]
    Thread.current.define_singleton_method(:body=) {|new_body| body = new_body }
    Thread.current.define_singleton_method(:body) { body }
    puts('%s is accepted' % s)

    s.print(IAC_DO_LINEMODE)
    s.print(IAC_SB_LINEMODE_0_IAC_SE)
    s.print(IAC_WILL_ECHO)

    begin
      sleep 0.1
      s.read_nonblock(SKIP_SIZE)
    rescue IO::EAGAINWaitReadable
      retry
    end

    s.erase_all
    s.move_cursor_lefttop

    loop {
      begin
        direction = s.read_nonblock(3)
      rescue IO::EAGAINWaitReadable
        nil
      rescue EOFError
        break
      end

      s.render
      break unless Thread.current.live
    } rescue nil

    puts('%s is gone' % s)
    s.close
    threads.delete(Thread.current)
  }
}

くふうしたところ: telnet

対戦するためのクライアントとして流用しました。 しかし最近のmacOSにはtelnet入っていないのでbrewで入れないといけません。

以下2点のやり方が分からなくてぐぐって出てきたstackoverflowの記事とRFCを参考に書きました。

参考

くふうしたところ: 表示

画面サイズは80x24固定にしてソースコードはそのまま、背景色だけ変えるようにしました。 この手法は先行事例のquinesnakeを参考にしています。

taylorconor/quinesnake: A quine that plays snake over its own source!

当初は以下のようにやる予定でした

  • 初期状態では餌をn*mサイズで置き、全てのソースコードをそこに詰め込む
  • 常に餌のブロックを表示しておけばソースコードが常に表示されつづける

しかし以下のような問題がありそうなことに気づき、背景色でプレイヤーを表示することにしました。

  • 餌の置き場がないほど🐍が巨大化するとソースコードが表示できなくなる
  • かといって🐍の体自体をソースコードにすると各プレイヤーがどの🐍を操作しているのか一瞥してわかりづらくなる
  • ソースコードの文字数を数えると80x24でギリギリ入るぐらい肥大化していて餌の場所だけだと全部のソースコードを収めきれない

整形のところはペンさんにやり方きいたり背景色の表示等いろいろ先行事例のQuineを参考にさせてもらいました。

コントロールシーケンスについてはここのウェブサイトも参考にしました。 ctlseqs(ms)

くふうしたところ: プレイヤーの表現

define_singleton_methodでThreadのインスタンス自身にプレイヤー関連のメソッドを直接はやしプレイヤーの機能をもたせました。プレイヤーのような動きをするThreadオブジェクトがあればそれはプレイヤー。Rubyぽい。

実際にはdefine_singleton_methodは長いので以下のようにMethodオブジェクトを取り出してから使いまわし、プレイヤー関連のメソッドを定義しています。

f=T.current.:define_singleton_method
f.(:p){q}
f.(:d){d}
f.(:e){l=false}

くふうしたところ: Ruby 2.7を使う

文字数が圧縮できます。

# > 2.7
Thread.current.method(:define_singleton_method)
# >= 2.7 メソッド参照演算子
Thread.current.:define_singleton_method

# 名前付けェ...
[[x,y]].each{|x,y|x+y}
# Numbered Parameters最の高
[[x,y]].each{@1+@2}

くふうしたところ: 壁や自分の体にぶつからない

やはり対戦型だと奇襲できたほうがいいので上下左右は連結させてある。↓にいくと↑から出るし、→に行くと←から出る。逆もまた然り。

自分の体にぶつかるような判定になると餌をとるモチベーションがわかないというのを実装中にかびさんに相談して教えてもらた。

反省点: ソースコードの整形は下の方でやる

上の方に整形処理を書くと整形処理のソースコードの文字数が変わったせいでうまく動かなくなったり下の行の再調整が必要になる。最後の方の行でやるのがよさそう。

反省点: 使っていないコード、雑すぎる;での位置合わせ

;で調整しているんですがめちゃくちゃ不要な;あるのがおわかりだろうか。

あとRefinementsを定義してるんだけどぱっとみ使っていない。これに関してはRubyがべんりすぎてわざわざ自分でメソッド定義しなくてもべんりなのがわるいので最高にいい話。

反省点: 最新のrubyでしか動かない

まあみんな手元でtrunk動かしているだろうしあまり問題ではないかも。

反省点: クライアントがtelnet

結構文字数に余裕があるのでサーバークライアント両方Quineの中に収めたほうがよかったかも。 とくにbrew installしないとtelnet入ってないのが手軽に試せなくて痛い。 まあでもみんなtelnetぐらいは入れていると思うので大丈夫そう。

困ったところ: 空白文字が消える

空白文字を直接ソースコードに書くとeval$c=%w()で包んでる関係で消えてしまうので0x20.chrするのを忘れないようにする。

困ったところ: 文字列のエスケープが行末にくると改行がエスケープされてしまう

これ本当に困った。

こういう文字列が

"\x1b[48;5;1m%s\x1b[0m"

ここで改行されたときね。気合で合わせる。

"\x1b[48;5;1m%s\
x1b[0m"

まとめ

  • すごいQuineと戦うため対戦型Quineを書いてQuineで戦う手法を示した。
  • ruby 2.7はべんり
  • Quineはじめませんか? 夏なので。1

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