Ducksay (#52)

by Jonas Pfenniger

__________________________________
< Welcome to this week's Ruby Quiz >
----------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||

This week, we will make some ascii-art for fun.

Produce a script that generates funny talking animals. The executable consists of three parts.

Part One : The balloon
=====================

The balloon is the surrounding part, in which the text will be shown. You can make it like you want.

Examples :

One-liner
_______________
< I love ruby ! >
---------------

Multiple lines with text wrapping and carriage return
__________________________________________
/ Q:How do you stop an elephant from \
| charging? |
\ A:Take away his credit cards. /
------------------------------------------

Part Two : The template
=======================

To make it flexible, we'll need a template language that can combine the animal and the text. You'll have to think about how you want to make it flexible enough.

Example :

ruby
# thoughts is the balloon's tail
def animal(thoughts = '\', eyes = 'oo', tongue=' ')
%[#{baloon}
#{thoughts} ^__^
#{thoughts} (#{eyes})\\_______
(__)\\ )\\/\\
#{tongue} ||----w |
|| ||
]

end

Part Three : The command-line arguments
=======================================

There are no special arguments. I would suggest asking for a text input, but you can also get the text from `fortune` or make it editable in place. Optional arguments can be given to change the selected template and some of its variables like the eyes, the tongue, and the balloon's tail.

Use your imagination !

So that's it for the quiz. Extra points are given to the person who provides a duck template.

Credits
=======

- cowsay : http://www.cowsay.net/


Quiz Summary

__________________
< class Cow < Duck >
------------------
\ _____
\ \\_-~~ ~~-_
\ /~ ~\
\ _| _ |
\ ___) ~~) ~~~--_ |
\ _-~ ~-_ ___ \ |/
\ / _-~ ~-_ /
\ | / \ |
\ | O) | | |
| | | |
| | (O | |
\ | | |\
(~-_ _-\ / _--_ / \
\__~~~ ~-_ _-~ / ~\
/ ~---~~-_ ~~~ _-~ /| |
_-~ / \ ~~--~ | | |
_-~ | / |
,-~~-_ __--~~ |-~ /
| \ | _-~
\ |--~~
\ | |
~-_ _ | |
~-_ ~~---__ _--~~\ |
~~--__ / |
~~---___ __--~~| |
~~~~~ | |
| |

That's using Ryan Leavengood's solution to display the much celebrated line from Dave Burt's solution. It's just so Ruby!

It looks like people had plenty of fun solving this quiz, which is great. We're all about The Fun Factor(tm) here at Ruby Quiz.

Let's get to a solution:

______________________
< Dave Burt's Solution >
----------------------
\
\ .-"""-. _.---..-;
:.) ;"" \/
__..--'\ ;-"""-. ;._
`-.___.^.___.'-.____J__/-._J

Dave is always teaching me great Ruby tricks in his solutions and this week's trick of choice is right at the top of the program:

ruby
class String
def width
inject(0) {|w, line| [w, line.chomp.size].max }
end
def height
to_a.size
end
def top
to_a.first
end
def middle
to_a.values_at(1..-1)
end
def bottom
to_a.last
end
end

# ...

I just know I would have written width() as:

ruby
map { |line| line.chomp.size }.max

Dave's is more efficient though. It only ever keeps the highest number, instead of building an Array of all the lengths. Just one more reason inject() is the one iterator to rule them all. Thanks for trick #562, Dave!

Obviously we're just dealing with some helpers added to String above, to make later work easier. Just remember that String.each() traverses lines by default and inject() and to_a() are defined in terms of each() and there shouldn't be any surprises here.

ruby
# ...

class Duck
def self.say(speech="quack?", *args)
balloon(speech) + body(*args)
end

def self.balloon(speech)
" _#{ '_' * speech.width }_\n" +
if speech.chomp =~ /\n/
"/ %-#{ speech.width }s \\\n" % speech.top.chomp +
speech.middle.map do |line|
"| %-#{ speech.width }s |\n" % line.chomp
end.join +
"\\ %-#{ speech.width }s /\n" % speech.bottom.chomp
else
"< #{ speech.chomp } >\n"
end +
" -#{ '-' * speech.width }-\n"
end

def self.body(thoughts='\\', eyes='cc', tongue=' ')
" #{thoughts}
#{thoughts}
_ ___
/ \\ / \\
\\. |: #{eyes}|
(.|:,---,
(.|: \\( |
(. y-'

\\ _ / #{tongue}
m m
"
end
end

# ...

The say() method should be easy enough to digest. Build a balloon() and a body(), slap them together, and return them. Beyond that, body() is almost right out of the quiz. It just builds a String, interpolating the substitutions.

That leaves balloon(). It's really just building a String too, with a little more logic thrown in. The if branch handles multiline comments, so the speech bubble can be rounded at the edges. Otherwise the else branch is used. The rest is just border drawing.

ruby
# ...

class Cow < Duck
def self.body(thoughts='\\', eyes='oo', tongue=' ')
" #{thoughts} ^__^
#{thoughts} (#{eyes})\\_______
(__)\\ )\\/\\
#{tongue} ||----w |
|| ||
"
end
end

class DuckOnWater < Duck
def self.body(thoughts='
\\', eyes='ยบ', tongue='>')
" #{thoughts}
` #{tongue[0, 1]}(#{eyes[0, 1]})____,
(` =~~/
~^~^~^~^~`---'
^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~^~
"
end
end

# ...

From there, other animals can just inherit and define a new body() method. Now we all truly understand Duck Typing! Everything is really just a Duck...

ruby
# ...

if $0 == __FILE__
if ARGV.include?("--help")
puts "usage: #$0 animal thoughts eyes tongue <speech\n"
puts "animals: Duck Cow DuckOnWater\n"
puts "e.g.: #$0 DuckOnWater o x ) </etc/fortune\n"
else
animal = Object.const_get(ARGV.shift) rescue Duck
puts animal.say(STDIN.read, *ARGV)
end
end

The main code is trivial. Show usage (if branch), or locate the proper class with const_get() and call say() (else branch). That's all it really takes to get the animals talking.

_________________________________________________________________
< Ryan Leavengood's Solution: Where even Chunky Bacon can talk... >
-----------------------------------------------------------------
\
\ __ _.._
.-'__`-._.'.--.'.__.,
/--' '-._.' '-._./
/__.--._.--._.'``-.__/
'._.-'-._.-._.-''-..'

I won't show the whole thing here, but do look it over. It had a nice collection of animals embedded in the DATA section of the code. They were controlled through a Zoo class, where even a random animal could be chosen to escape():

ruby
# ...

# Our lovely Zoo, full of many wacky animals
class Zoo
include Singleton
attr_reader :animals
def initialize
@animals = {}
current_key = nil
DATA.each do |line|
if line.chomp =~ /^'(\w*)':$/
current_key = $1
@animals[current_key] = []
elsif current_key
@animals[current_key] << line
end
end
end

# A random animal has escaped!
def escape
choices = @animals.keys
@animals[choices[rand(choices.length)]]
end
end

# ...

Ryan also emulated fortune for Windows, with only a few lines of code:

ruby
require 'open-uri'

# ...

# Since I wrote this on Windows I don't have fortune...but the
# internet does!
def get_fortune
open('http://www.coe.neu.edu/cgi-bin/fortune') do |page|
page.read.scan(/<pre>(.*)<\/pre>/m)[0][0].gsub("\t",' ')
end
end

# ...

Fun to read and educational at the same time. You can't beat that folks!

________________
< The Other Guys >
----------------
\
\
_ ___
/ \ / \
\. |: ^-|
(.|:,---,
(.|: \( |
(. y-'
\ _ /
m m

Jacob Quinn Shenker golfed the solution, needing less than 400 strokes. Check it out.

JB Eriksson supported thought bubbles and sent in a duck on water template. More good stuff.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
(Wrap Up: This went just swimmingly!)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
o
` >(o)____,
(` =~~/
^~^~^~^~^~`---'^~^~^~^~^~

I have to offer major thanks to everyone who made is possible for me to fill a quiz summary with animal drawings! How cool is that?

Tomorrow's quiz takes us from animals to appliances as we build our own DRYer...