Building an ASCII Quine
I was watching some of the RubyConf stuff today and I stumbled upon this talk. The talk was given by Yusuke Endoh and he is an absolute wizard. He has a bunch of these quines on his blog and they are really crazy. There are examples where he plays music, makes a revolving globe quine, and makes it snow. I would check these out if you have time.
I wanted to try some of his suggestions to build my own. Since I have been working on this chatbot, I thought it would be cool for the program to ask the user a question, hit api.ai, respond with an answer, and then print out the code. Because of this, it isn’t a true quine. What is a Quine?
For those who aren’t familiar, according to Wikipedia:
A quine is a non-empty computer program that takes no input and produces a copy of its own source code as its only output.
It seems pretty difficult right? Luckily there are a few tricks in ruby that make this possible.
Here is the end product. You are supposed to be able to see EVAN but it is a little hard on the webpage.
eval(s=%q(s="eval(s=%q(#{s}))";eval(%w(;require'net/http';require'
json';token=ENV['API_AI_CLIENT_TOKEN'];str=%w(80@108@101@97@115@10
1@32@100@101@102@105@110@101@32@65@80@73@95@65@73@95@67@76@73@69@7
8@84@95@84@79@75@69@78).join.split('@').map(&:to_i).map(&:chr).joi
n;throw(str)if(token.nil?);str=%w(72@105@44@32@73@39@109@32@69@118
@97@110@ 39@115
@32@99@ 104@97@116@ 98@11 1@116@4 6@32@87 @104@97 @11 6@32@1
05@115@ 32@121@111@1 17@114 @32@1 13@117@1 01@115@1 16@1 05@111@
110@32@ 102@11 1@114@ 32@10 9@1 01@6 3).join. spli t('@').
map(&: to_i).map(& :chr).join; puts(str) ;question=ge ts;para
ms={v: '20170712',q uery:questio n,lang:'en ',se ssionId: rand}.ma
p{|k,v |"#{k }=#{v}"}.j oin( '&'); url= "https:/ /api.dia
logfl ow.com/v1/q uery?#{par ams}" ;uri =UR I(url); req=Net:
:HTTP ::Get.new(ur i);req['A uthori zati on'] ="Bearer \s#{token
}";re s=Net::HT
TP.start(uri.hostname,uri.port,use_ssl:true){|http|http.request(re
q)};body=JSON.parse(res.body);puts(body['result']['fulfillment']['
speech']);sleep(1);str=%w(72@101@114@101@32@105@115@32@116@104@101
@32@115@111@117@114@99@101@32@99@111@100@101@58@32@45@45@45@45@45@
62).join.split('@').map(&:to_i).map(&:chr).join;puts(str);sleep(1)
;)*"");puts s)) ##### Made by Evan Dancer (https://evandancer.com)
In case it is a little difficult to see, here is the pure ascii of what I wanted to generate. It is my name. Really creative right?
##################################################################
##################################################################
##################################################################
##################################################################
##################################################################
######## ######
####### ########### ##### ####### ####### ####### ### ######
####### ############ ###### ##### ######## ######## #### #######
####### ###### ###### ##### ### #### ######## #### #######
###### ########### ########### ######### ############ #######
###### ############ ############ ########## #### ######## ########
###### ##### ########## #### ##### #### ######## ########
##### ########### ########## ##### #### ### ####### ########
##### ############ ######### ###### #### #### ######## #########
##### #########
##################################################################
##################################################################
##################################################################
##################################################################
##################################################################
Here is what happens when you run the program.
ruby quine.rb
Hi, I'm Evan's chatbot. What is your question for me?
What is your favorite color?
My favorite color is blue. Go Blue!
Here is the source code: ----->
...
Alright, let’s get started. I first built a simple ruby program that used my dialogflow to answer chatbot questions. You can ask the same questions to my chatbot on the home page. Here is the code that I wrote to do this. I’m not going to go into the details of this script in this post but it should be pretty self-explanatory.
## Need these two libraries
require 'net/http'
require 'json'
## Check for environment variable or throw exception
token = ENV['API_AI_CLIENT_TOKEN']
throw 'Please define API_AI_CLIENT_TOKEN' if token.nil?
## Ask the question
puts "Hi, I'm Evan's chatbot. What is your question for me?"
question = gets
## Build the url
params = {
v: '20170712',
query: question,
lang: 'en',
sessionId: rand
}.map { |k, v| "#{k}=#{v}" }.join('&')
url = "https://api.dialogflow.com/v1/query?#{params}"
## Use HTTP to send it
uri = URI(url)
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{token}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
## Parse it up
body = JSON.parse(res.body)
puts body['result']['fulfillment']['speech']
sleep(1)
puts 'Here is the source code: ----->'
sleep(1)
Trick #1
To convert this code into a quine, all you need to do is wrap it in this code:
eval s=%q(
s="eval s=%q(#{s})"
# Fun stuff goes here...like the code above.
puts s
)
Good work. We just created a quine. It is that simple.
Next, I wanted to work on formatting it as ASCII art. It isn’t exactly ASCII but I wanted it to say my name with the code. In order to do this, I had to use another trick.
Trick #2
In order to shape the text with arbitrary newlines and spaces, I used this code:
eval(%w(
# Code goes here...
).join('')
%w(foo bar)
is a shortcut for ["foo", "bar"]
. Joining the resulting array of strings will leave you with the original text. Running eval on top of that will execute your code.
Pro tip: You can use any character for parentheses. Square brackets,
%w[...]
, curly brackets%w{...}
, and even something like this%w@...@
sign works.
In addition to this, you need to also:
- Remove all whitespace in your code (Yes…it won’t look pretty)
- Use semicolons between lines of code to terminate expressions (since everything will be on the same line)
- Remove comments.
I removed all of the spaces in my code (except for the ones in the string). The following code is a little hard on the eyes. Rubocop was not happy with me. But we are making progress.
require'net/http';
require'json';
token=ENV['API_AI_CLIENT_TOKEN'];
throw'Please define API_AI_CLIENT_TOKEN'if(token.nil?);
puts"Hi, I'm Evan's chatbot. What is your question for me?";
question=gets;
params={v:'20170712',query:question,lang:'en',sessionId:rand}.map{|k,v|"#{k}=#{v}"}.join('&');
url="https://api.dialogflow.com/v1/query?#{params}";
uri=URI(url);
req=Net::HTTP::Get.new(uri);
req['Authorization']="Bearer #{token}";
res=Net::HTTP.start(uri.hostname,uri.port,use_ssl:true){|http|http.request(req)};
body=JSON.parse(res.body);
puts(body['result']['fulfillment']['speech']);
sleep(1);
puts'Here is the source code: ----->';
sleep(1);
Nice! It looks like our code still runs which is key. Now we need to get rid of a bunch of these whitespaces in our strings which brings me to the next trick.
Trick #3
I encoded my strings in a unique way. This was mostly to use denser characters to make the ASCII art pop more. I’m not sure if replacing the spaces with \s
would work. I wanted something bulkier.
I wanted to convert the string into an array of character codes and then join the characters together to make a string. This way, I will be able to use some numbers which will be larger and random looking.
I wrapped everything in %w
. This works best as we talked about above. I added @ chars to split the char codes apart because they were a dense character.
str = %w(72@101@108@108@111).join.split('@').map(&:to_i).map(&:chr).join
# str == "Hello"
I built a little script to help me convert my strings:
str = gets
str.strip!
ascii = str.split('').map(&:ord).join('@')
puts "str=%w(#{ascii}).join.split('@').map(&:to_i).map(&:chr).join"
Final steps
I wrapped my code with step 1 and 2 and removed all the newlines. I was left with this ugly pile of code:
eval(s=%q(s="eval(s=%q(#{s}))";eval(%w(;require'net/http';require'json';token=ENV['API_AI_CLIENT_TOKEN'];str=%w(80@108@101@97@115@101@32@100@101@102@105@110@101@32@65@80@73@95@65@73@95@67@76@73@69@78@84@95@84@79@75@69@78).join.split('@').map(&:to_i).map(&:chr).join;throw(str)if(token.nil?);str=%w(72@105@44@32@73@39@109@32@69@118@97@110@39@115@32@99@104@97@116@98@111@116@46@32@87@104@97@116@32@105@115@32@121@111@117@114@32@113@117@101@115@116@105@111@110@32@102@111@114@32@109@101@63).join.split('@').map(&:to_i).map(&:chr).join;puts(str);question=gets;params={v:'20170712',query:question,lang:'en',sessionId:rand}.map{|k,v|"#{k}=#{v}"}.join('&');url="https://api.dialogflow.com/v1/query?#{params}";uri=URI(url);req=Net::HTTP::Get.new(uri);req['Authorization']="Bearer\s#{token}";res=Net::HTTP.start(uri.hostname,uri.port,use_ssl:true){|http|http.request(req)};body=JSON.parse(res.body);puts(body['result']['fulfillment']['speech']);sleep(1);str=%w(72@101@114@101@32@105@115@32@116@104@101@32@115@111@117@114@99@101@32@99@111@100@101@58@32@45@45@45@45@45@62).join.split('@').map(&:to_i).map(&:chr).join;puts(str);sleep(1);)*"");puts s))
Now I have to format it into the real thing. I built this quick little script to help me.
code = File.open('input.rb', 'rb').read.strip
str = '
##################################################################
##################################################################
##################################################################
##################################################################
##################################################################
######## ######
####### ########### ##### ####### ####### ####### ### ######
####### ############ ###### ##### ######## ######## #### #######
####### ###### ###### ##### ### #### ######## #### #######
###### ########### ########### ######### ############ #######
###### ############ ############ ########## #### ######## ########
###### ##### ########## #### ##### #### ######## ########
##### ########### ########## ##### #### ### ####### ########
##### ############ ######### ###### #### #### ######## #########
##### #########
##################################################################
##################################################################
##################################################################
##################################################################
##################################################################
'.strip
code_len = code.length
ascii_len = str.scan(/\S/).count
diff = ascii_len - code_len
puts "Code length: #{code_len}"
puts "ASCII length: #{ascii_len}"
puts "Diff: #{ascii_len - code_len}"
throw 'ascii too small' if diff < 0
code_chars = code.split('')
formatted = str.split('').map { |x| x == '#' ? code_chars.shift : x }.join('')
File.open('quine.rb', 'w') do |f|
f.write(formatted)
end
And there you have it. I was able to write my first program:
- Spelled my name with the code.
- Printed its own source code.
- Answered questions about myself.