Oops! Sorry for the large number of ads on here. Long story short I goofed the setup. I knew they wouldn't show up for me, due to Adblock Plus. (One of my most favoritest Firefox plugins EVAR!) What I didn't realize is that it would also block their representation on the Blogger layout design screen! So I had a bunch, at the top, when I thought I had none, and intended to just put one at the bottom. Mega chalupa!
Adventures of a dinosaur evolving to meet today's challenges.
MOST OLD CONTENT HAS MOVED; see new home at
https://www.codosaur.us/blog/
Tuesday, October 5, 2010
Sunday, October 3, 2010
Rosetta Stone: the company, and my own
In other news, I have accepted a post with Rosetta Stone, in Rosslyn VA, and will be starting on October 18. Oddly enough, I'll be working mainly in ActionScript and Flex... neither of which I know yet! I'm told that ActionScript is generally similar to JavaScript, which I do know, so at least that's a head-start.
What does this mean for you, my loyal readers? I may soon start doing some of these katas in those languages. Are you disappointed that I'll be discontinuing Ruby? You were bound to be disappointed that way eventually anyway. My plan has long been to build a matrix of problems (such as Dave Thomas katas, other katas, normal life, N-dimensional life, multi-lifeform life, etc.), versus languages, with links to blog posts showing solutions for each problem in each language. Sort of... my own little electronic Rosetta Stone.
Life in N Dimensions
Sometimes we all feel as though life has gotten a bit out of hand. It's certainly been that way for me lately. As you may have noticed, I haven't updated this blog in quite some time, despite the way I was obviously trying to do it weekly. Was this just yet another case of someone up and walking away from their blog? No, not me! Really! I promise! (Well, at least this time it wasn't....)
So what's been keeping me so busy? My life has been very busy in several dimensions. I'll spare you most of it, but one was Ruby DCamp, an "unconference" centered around Ruby. The first day featured a "code retreat". Not an official one with Corey Haines himself present, but at least an event of that style. Apparently, the canonical problem to tackle in these is Conway's Game of Life.
Since each round is only 45 minutes, I didn't manage to get all the way through a successful implementation. And since it's so simple, I won't bother you with the one I did later.
BUT...
Evan Light, the organizer of Ruby DCamp, sort of threw down a gauntlet about extending Life from two dimensions to... unlimited. So, of course I then spent much of my spare time figuring that out.
The really hard part was to figure out how to display the state of the world. Eventually I settled on a sort of recursive concept:
- An individual cell is just output as-is.
- A row of cells (which I represented internally as a String) is output as-is, again.
- A two-dimensional world was output as a grid, as usual.
- For any higher dimensions, though:
- If it's odd, it's output as a row of blocks from the next dimension down, with a border between each and around the whole set.
- If it's even, it's output as a block of rows from the next dimension down, with a border between each and around the whole set.
- If it's odd, it's output as a row of blocks from the next dimension down, with a border between each and around the whole set.
Yeah, I know, that sounds kinda complex. But take a look at it in action, and you'll see what I mean. For instance, a three-dimensional world would look like this:
|* *| * | *| |***| **|* | |** | * |* |a four-dimensional world would look like this:
+---+---+---+ |* *| * | *| |***| **|* | |** | * |* | +---+---+---+ |* *| **| *| | **| **|* *| |** | *|* | +---+---+---+ |* *| * | *| |* *| **|***| |** |** |* *| +---+---+---+a five-dimensional world would look like this:
|+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||***| **|* |||***| **|* |||***| **|* || ||** | * |* |||** | * |* |||** | * |* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| **| *|||* *| **| *|||* *| **| *|| || **| **|* *||| **| **|* *||| **| **|* *|| ||** | *|* |||** | *|* |||** | *|* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||* *| **|***|||* *| **|***|||* *| **|***|| ||** |** |* *|||** |** |* *|||** |** |* *|| |+---+---+---+|+---+---+---+|+---+---+---+|a six-dimensional world would look like this:
+-------------+-------------+-------------+ |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||***| **|* |||***| **|* |||***| **|* || ||** | * |* |||** | * |* |||** | * |* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| **| *|||* *| **| *|||* *| **| *|| || **| **|* *||| **| **|* *||| **| **|* *|| ||** | *|* |||** | *|* |||** | *|* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||* *| **|***|||* *| **|***|||* *| **|***|| ||** |** |* *|||** |** |* *|||** |** |* *|| |+---+---+---+|+---+---+---+|+---+---+---+| +-------------+-------------+-------------+ |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||***| **|* |||***| **|* |||***| **|* || ||** | * |* |||** | * |* |||** | * |* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| **| *|||* *| **| *|||* *| **| *|| || **| **|* *||| **| **|* *||| **| **|* *|| ||** | *|* |||** | *|* |||** | *|* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||* *| **|***|||* *| **|***|||* *| **|***|| ||** |** |* *|||** |** |* *|||** |** |* *|| |+---+---+---+|+---+---+---+|+---+---+---+| +-------------+-------------+-------------+ |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||***| **|* |||***| **|* |||***| **|* || ||** | * |* |||** | * |* |||** | * |* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| **| *|||* *| **| *|||* *| **| *|| || **| **|* *||| **| **|* *||| **| **|* *|| ||** | *|* |||** | *|* |||** | *|* || |+---+---+---+|+---+---+---+|+---+---+---+| ||* *| * | *|||* *| * | *|||* *| * | *|| ||* *| **|***|||* *| **|***|||* *| **|***|| ||** |** |* *|||** |** |* *|||** |** |* *|| |+---+---+---+|+---+---+---+|+---+---+---+| +-------------+-------------+-------------+and so on.
And now, what you've all been waiting for... the code:
#!/usr/bin/ruby # first some general utility stuff # combine several blocks of text, as columns. # might be some more elegant way to do this with recursive # calls to Array#zip or something like that.... def combine_columns blocks, separator = '|', border = true exploded = blocks.map { |blk| blk.split "\n" } num_rows = exploded[0].length result = [] num_rows.times do |row_num| row = exploded.map { |blk| blk[row_num] } if border then row.push '' row.unshift '' end result << row.join(separator) end result.join "\n" end # combine several rows of text, with separators made of a given char, using an # alternate char wherever another certain char appears in the top or bottom of # the row, assuming that they all start (and end) with the same thing as the # start of the first. def combine_rows rows, norm_char = '-', except = '|', response = '+', border = true first_line = rows[0] len = first_line.index("\n") sep = norm_char * len start = 0 while not (start.nil? or start >= len) start = first_line.index except, start if not start.nil? sep[start] = response start += 1 end end tmp = rows.join("\n#{sep}\n") tmp = "#{sep}\n#{tmp}\n#{sep}" if border end # now the stuff specifically for this app: class World # apply the basic rules of life and death # TODO MAYBE: Speed this up. This is where the profiler says it spends # the second most time out of all the parts I wrote. def apply_rule cur, nbrs if cur == '*' then (nbrs >= @min_stay and nbrs <= @max_stay ) ? '*' : ' ' else (nbrs >= @min_birth and nbrs <= @max_birth) ? '*' : ' ' end end # public only for external testing purposes def count_neighbors coords count_neighbors_helper [], coords, coords, @cells end def initialize opts @cells = opts[:cells] @num_dims = get_num_dims_helper @cells # Note, these formulas are rather ad-hoc. Just my SWAG at how to adjust # for varying number of dimensions, and give the "standard" results in 2d. base = 2 ** @num_dims @min_stay = opts.fetch :min_stay, base / 2 @max_stay = opts.fetch :max_stay, base - 1 @min_birth = opts.fetch :min_birth, base / 2 + 1 @max_birth = opts.fetch :max_birth, base - 1 @max_stay = @min_stay if @max_stay < @min_stay @max_birth = @min_birth if @max_birth < @min_birth end # convert current state to a string, for display purposes def to_s to_s_helper @cells, @num_dims end # let the world turn.... def turn @cells = turn_helper @cells, [] end private # see what's in a particular subset of cells neighboring a given center # TODO MAYBE: Speed this up. This is where the profiler says it spends # the most time out of all the parts I wrote. def count_neighbors_helper coords_done, coords_todo, center, cells todo_dup = coords_todo.dup count = 0 next_coord = todo_dup.shift ((next_coord - 1) .. (next_coord + 1)).each do |idx| next if idx < 0 or idx >= cells.length done_dup = coords_done.dup done_dup << idx if cells.class == Array count += count_neighbors_helper done_dup, todo_dup, center, cells[idx] elsif done_dup != center and cells[idx..idx] == '*' count += 1 end end count end # figure out how many dimensions are in this world. # (need this in order to set default rule parameters.) def get_num_dims_helper cells if cells.class == Array: get_num_dims_helper(cells[0]) + 1 elsif cells.class == String: 1 else puts "ERROR: cells must be of type [Array of ...] String" puts "(i.e., String, Array of String, Array of Array of String, etc.)" # TODO: throw an exception? exit 1 end end # turn a chunk of the world into a string, # by recursing if it's too big to do at once (3+ dimensions). # 1d is a simple string (should only happen in a total 1d world), # 2d is a block of strings printed atop each other, # higher odd dimensions are rows of blocks, # higher even dimensions are blocks of rows. def to_s_helper cells, num_dims if num_dims == 1: cells elsif num_dims == 2: cells.join("\n") elsif num_dims % 2 == 0 then rows = cells.map { |row| to_s_helper(row, num_dims - 1) } combine_rows rows else blocks = cells.map { |blk| to_s_helper blk, num_dims-1 } combine_columns blocks end end # turn the world a piece at a time, drilling down into subsections as needed, # accumulating results as we go def turn_helper cells, coords results = [] cells.length.times do |idx| coords_dup = coords.dup coords_dup << idx if cells.class == Array: results << turn_helper(cells[idx], coords_dup) else results << apply_rule(cells[idx..idx], count_neighbors(coords_dup)) end end results = results.join if cells.class == String results end end
But wait! There's more! You get a special bonus: the tests used to develop this program. Sorry they're not complete, but as you can probably guess, the ones I've left blank would be horribly tedious to fill in.
#! /usr/bin/ruby require 'nd_life' require 'profile' describe 'nd_life' do it 'should apply altered rules correctly' do w = World.new :cells => '', :min_stay => 4, :max_stay => 6, :min_birth => 2, :max_birth => 5 w.apply_rule('*', 3).should == ' ' w.apply_rule('*', 4).should == '*' w.apply_rule('*', 6).should == '*' w.apply_rule('*', 7).should == ' ' w.apply_rule(' ', 1).should == ' ' w.apply_rule(' ', 2).should == '*' w.apply_rule(' ', 5).should == '*' w.apply_rule(' ', 6).should == ' ' end it 'should apply default 1d rules correctly' do w = World.new :cells => '' w.apply_rule('*', 0).should == ' ' w.apply_rule('*', 1).should == '*' w.apply_rule('*', 2).should == ' ' w.apply_rule(' ', 1).should == ' ' w.apply_rule(' ', 2).should == '*' w.apply_rule(' ', 3).should == ' ' end it 'should apply default 2d rules correctly' do w = World.new :cells => [''] w.apply_rule('*', 1).should == ' ' w.apply_rule('*', 2).should == '*' w.apply_rule('*', 3).should == '*' w.apply_rule('*', 4).should == ' ' w.apply_rule(' ', 2).should == ' ' w.apply_rule(' ', 3).should == '*' w.apply_rule(' ', 4).should == ' ' end it 'should apply default 3d rules correctly' do w = World.new :cells => [['']] w.apply_rule('*', 3).should == ' ' w.apply_rule('*', 4).should == '*' w.apply_rule('*', 7).should == '*' w.apply_rule('*', 8).should == ' ' w.apply_rule(' ', 4).should == ' ' w.apply_rule(' ', 5).should == '*' w.apply_rule(' ', 7).should == '*' w.apply_rule(' ', 8).should == ' ' end it 'should apply default 4d rules correctly' do w = World.new :cells => [[['']]] w.apply_rule('*', 7).should == ' ' w.apply_rule('*', 8).should == '*' w.apply_rule('*', 15).should == '*' w.apply_rule('*', 16).should == ' ' w.apply_rule(' ', 8).should == ' ' w.apply_rule(' ', 9).should == '*' w.apply_rule(' ', 15).should == '*' w.apply_rule(' ', 16).should == ' ' end it 'should count 1d neighbors correctly' do w = World.new :cells => '* ** ' w.count_neighbors([0]).should == 0 w.count_neighbors([1]).should == 2 w.count_neighbors([2]).should == 1 w.count_neighbors([3]).should == 1 w.count_neighbors([4]).should == 1 end it 'should count 2d neighbors correctly' do w = World.new :cells => ['* ', ' * ', ' **'] w.count_neighbors([0,0]).should == 1 w.count_neighbors([0,1]).should == 2 w.count_neighbors([0,2]).should == 1 w.count_neighbors([1,0]).should == 3 w.count_neighbors([1,1]).should == 3 w.count_neighbors([1,2]).should == 3 w.count_neighbors([2,0]).should == 2 w.count_neighbors([2,1]).should == 2 w.count_neighbors([2,2]).should == 2 end it 'should count 3d neighbors correctly' do c = [['* ', ' '], [' *', '**'], ['* ', ' *']] w = World.new :cells => c w.count_neighbors([0,0,0]).should == 3 w.count_neighbors([0,0,1]).should == 4 w.count_neighbors([0,1,0]).should == 4 w.count_neighbors([0,1,1]).should == 4 w.count_neighbors([1,0,0]).should == 6 w.count_neighbors([1,0,1]).should == 5 w.count_neighbors([1,1,0]).should == 5 w.count_neighbors([1,1,1]).should == 5 w.count_neighbors([2,0,0]).should == 4 w.count_neighbors([2,0,1]).should == 5 w.count_neighbors([2,1,0]).should == 5 w.count_neighbors([2,1,1]).should == 4 end it 'should count 4d neighbors correctly' it 'should generate the next setup correctly in 1d' do w = World.new :cells => '* ** ' c_str = w.to_s neighbor_counts = {} c_str.length.times { |idx| neighbor_counts[idx] = w.count_neighbors [idx] } w.turn c2_str = w.to_s c_str.length.times do |idx| old = c_str[idx..idx] new = c2_str[idx..idx] nbrs = neighbor_counts[idx] new.should == w.apply_rule(old, nbrs) end end it 'should generate the next setup correctly in 2d' do c = ['* ', ' * ', ' **'] w = World.new :cells => c c_str = w.to_s neighbor_counts = {} c.length.times do |row_idx| c[row_idx].length.times do |col_idx| neighbor_counts["#{row_idx}_#{col_idx}"] = w.count_neighbors [row_idx, col_idx] end end w.turn c2_str = w.to_s c.length.times do |row_idx| cur_row = c[row_idx] cur_row.length.times do |col_idx| idx = row_idx * (cur_row.length + 1) + col_idx old = c_str[idx..idx] new = c2_str[idx..idx] nbrs = neighbor_counts["#{row_idx}_#{col_idx}"] new.should == w.apply_rule(old, nbrs) end end end it 'should generate the next setup correctly in 3d' it 'should generate the next setup correctly in 4d' it 'should store a 1d setup correctly' do c = '* ' w = World.new :cells => c w.to_s.should == c end it 'should store a 2d setup correctly' do c = ['* ', ' * ', ' **'] w = World.new :cells => c w.to_s.should == c.join("\n") end it 'should store a 3d setup correctly' do c = [['* ', '* ', ' * '], [' **', ' * ', '* *'], ['** ', '** ', ' **']] w = World.new :cells => c w.to_s.should == "|* | **|** |\n"\ "|* | * |** |\n"\ "| * |* *| **|" end it 'should store a 4d setup correctly' do c = [[['* ', '* ', ' * '], [' **', ' * ', '* *'], ['** ', '** ', ' **']], [['* ', '* ', ' * '], [' **', ' * ', '* *'], ['** ', '** ', ' **']], [['* ', '* ', ' * '], [' **', ' * ', '* *'], ['** ', '** ', ' **']]] w = World.new :cells => c w.to_s.should == "+---+---+---+\n"\ "|* | **|** |\n"\ "|* | * |** |\n"\ "| * |* *| **|\n"\ "+---+---+---+\n"\ "|* | **|** |\n"\ "|* | * |** |\n"\ "| * |* *| **|\n"\ "+---+---+---+\n"\ "|* | **|** |\n"\ "|* | * |** |\n"\ "| * |* *| **|\n"\ "+---+---+---+" end it 'should store a 5d setup correctly' it 'should store a 6d setup correctly' end
If you're especially observant (no I don't mean Orthodox!), you may have noticed that my previous tests were done with Test::Unit, and these are RSpec. Yes, I finally learned RSpec, at Ruby DCamp. Also note the profiler; that helped me spot the bottlenecks. (You may think you know where the bottlenecks in your code are, but chances are, you're wrong. That's just one of the things humans generally aren't very good at. Let the machine do the work and tell you where it hurts.)
And! If you order by midnight tonight, you get a special second bonus: a test driver, so you can watch this thing run, in six whopping dimensions! Just look:
#!/usr/bin/ruby require 'nd_life' # six dimensions cells = [[[[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]], [[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]], [[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]]], [[[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]], [[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]], [[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]]], [[[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]], [[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]], [[[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']], [[' ',' ',' '],[' ',' ',' '],[' ',' ',' ']]]]] def randomize cells if cells.class == Array then cells.map { |part| randomize part } else cells.length.times { |i| cells[i..i] = rand(2) == 1 ? '*' : ' ' } end end randomize cells w = World.new :cells => cells gen_num = 0 prev_worlds = [] stable = false rep = w.to_s while not stable do gen_num += 1 puts "Generation #{gen_num}:" puts rep prev_worlds << rep w.turn rep = w.to_s idx = prev_worlds.rindex rep # use rindex since later most likely to match if not idx.nil? stable = true puts "Next state is same as at Generation #{idx + 1}" end end
Now how much would you pay? (crickets . . . crickets . . .) Well that's okay, because it's not $999, not $99, not even $9, but free, though as always I would appreciate you donating the time to tell me how I'm Doin' It Wrong.
Seriously. This thing is pretty slow, at about seven seconds per generation, on my machine. (On the other claw, it is pretty ancient, a Powerbook, and yes I mean Powerbook not Macbook.) Let's hear some ideas for speeding it up. The profiler says the main timesink is the counting of neighbors, specifically count_neighbors_helper
. Any better, or at least other, ideas? Why would you do it your way?
As for other assorted improvements, I've already thought of, but decided not to do quite yet:
- Telling it only how many dimensions it has, and how big it is, letting it assume that all dimensions are the same size, and generate random contents.
- Telling it only how many dimensions it has, and how big each is, letting it generate random contents.
- Having it detect when stability has been reached. (Actually I had that in earlier, just detecting when a new generation was the same as the old, but since so many forms of stability are cyclic, I took it out, in favor of moving that functionality to the driver.)