Quineスネークバトル🐍
おもしろかったらStar https://github.com/hanachin/quine-snake
おもしろかったら💖して 🔁
telnetで対戦できるQuineなsnake gamehttps://t.co/hYCRTGdtSE pic.twitter.com/aziv7ai6yB
— 𝓜𝓲𝔂𝓪𝓰𝓲 (@hanachin_) August 4, 2019
複数プレイヤーいるとこういう感じ
モチベーション
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を参考に書きました。
参考
- java - Send data over telnet without pressing enter - Stack Overflow
- RFC 854 - Telnet Protocol Specification
- RFC 1184 - Telnet Linemode Option
- RFC 857 - Telnet Echo Option
くふうしたところ: 表示
画面サイズは80x24固定にしてソースコードはそのまま、背景色だけ変えるようにしました。 この手法は先行事例のquinesnakeを参考にしています。
taylorconor/quinesnake: A quine that plays snake over its own source!
当初は以下のようにやる予定でした
しかし以下のような問題がありそうなことに気づき、背景色でプレイヤーを表示することにしました。
- 餌の置き場がないほど🐍が巨大化するとソースコードが表示できなくなる
- かといって🐍の体自体をソースコードにすると各プレイヤーがどの🐍を操作しているのか一瞥してわかりづらくなる
- ソースコードの文字数を数えると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"