Drawing Celtic Knotwork 3

I want to hurry on to making pictures, so let's rush through the Grid part.

That's easy enough, actually. We only need to be able to do a few simple things with a Grid:

  • Create it at a set size.
  • Add a Tile somewhere in the Grid.
  • Read each individual pixel of the Grid transparently.

And here's the code.

# I am a single small section of a knotwork image. I know about my 
# dimensions, and can describe myself on a pixel-by-pixel basis.
class Tile
  
  def initialize(str = nil)
    @pixels = []
    (0..8).each { 
      row = []
      (0..8).each { row << nil }
      @pixels << row
    }
    
    if str then
      set_from_string(str)
    end
  end
  
  def at(x, y)
    return @pixels[x][y]
  end
  alias is_set? at
  
  def set(x, y, value=true)
    @pixels[x][y] = value
  end
  
  def unset(x, y)
    @pixels[x][y] = nil
    
    return true
  end
  
  def set_from_string(str)
    str.split("\n").each_with_index do |line, row|
      line.split(' ').each_with_index do |pixel, col|
        set(row, col, pixel)
      end
    end
  end
  
  def to_s
    str = ""
    @pixels.each { |row|
      str += "|"
      row.each { |pixel| 
        pixel ||= " "
        str += "#{pixel}|" 
      }
      str += "\n"
    }
    return str
  end
end

# I am an arranged collection of Tiles. I know how to add and remove
# Tiles along a 2-d grid, and can also present myself as if I were a single
# large Tile.
class Grid
  def initialize(rows, columns)
    @tile_size = 9
    @rows      = rows
    @columns   = columns
    @pixels    = Array.new(rows*@tile_size) { |i|
      Array.new(columns*@tile_size)
    }
  end
  
  def set_tile(row, column, tile)
    pixel_origin_x = row * @tile_size
    pixel_origin_y = column * @tile_size
    (0...@tile_size).each { |tile_x|
      x = pixel_origin_x + tile_x
      (0...@tile_size).each { |tile_y|
        y = pixel_origin_y + tile_y
        @pixels[x][y] = tile.at(tile_x, tile_y)
      }
    }
  end
  
  def at(row, column)
    return @pixels[row][column]
  end
  
  def to_s
    str = ""
    @pixels.each { |row|
      str += row.join(' ')
      str += "\n"
    }
    
    return str
  end
  
end


#####
# Test code
#####
$source_string =<<HERE
          x . . . . . . . x
          . x . . . . . x .
          . . x . . . x . .
          . . . x . x . . .
          . . . . x . . . .
          . . . x . x . . .
          . . x . . . x . .
          . x . . . . . x .
          x . . . . . . . x
HERE
$source_string.gsub!(/^\s+/m, '')

require 'test/unit'

class TC_Tile < Test::Unit::TestCase
  def setup
    @@tile = Tile.new()
  end
  
  def test_pixels
    assert_equal(nil, @@tile.is_set?(0, 0), 
                 "By default, any pixel in a Tile is blank")
    assert(@@tile.set(0, 0), 
           "Use Tile#set(row, col) to set a pixel at coordinates (row, col)")
    assert(@@tile.is_set?(0, 0), 
           "A pixel (row, col) is set after Tile#set(row, col) has been called")
    assert(@@tile.unset(0, 0), 
           "Use Tile#unset(row, col) to clear a pixel at coordinates (row, col)")
    assert_equal(nil, @@tile.is_set?(0, 0), 
                 "An unset pixel has no set value")
    @@tile.set(1, 1)
    assert_equal(nil, @@tile.is_set?(0, 0), 
                 "Setting one pixel has no effect on other pixels in a Tile")
    assert(@@tile.is_set?(1, 1), 
           "Tile remembers the set status of each pixel in its confines.")
    
    assert(@@tile.set_from_string($source_string),
           "You can use ASCII art strings to set the pixels in a Tile")
    assert(@@tile.is_set?(0, 0))
    assert(@@tile.is_set?(1, 0))
    assert_equal('x', @@tile.at(0, 0),
                 "A Tile remembers the value assigned, if given, " \
                 "during Tile#set(row, col, val)")
  end
end

class TC_Grid < Test::Unit::TestCase
  def test_simple_grid
    grid = Grid.new(1, 1)
    tile = Tile.new($source_string)
    grid.set_tile(0, 0, tile)
    assert_equal("x", grid.at(0, 0),
                 "Use Grid#pixel_at(row, col) to access pixel at " \
                 "(row, col) distance from upper left corner")
    assert_equal($source_string, grid.to_s)
  end
  
  def test_large_grid
    grid = Grid.new(1, 2)
    tile1 = Tile.new($source_string)
    tile2 = Tile.new($source_string)
    grid.set_tile(0, 0, tile1)
    grid.set_tile(0, 1, tile2)
    assert_equal("x", grid.at(0, 0))
    assert_equal("x", grid.at(0, 9),
                 "Grid#pixel_at uses whole grid as coordinate system")
    expected_output =<<HERE
          x . . . . . . . x x . . . . . . . x
          . x . . . . . x . . x . . . . . x .
          . . x . . . x . . . . x . . . x . .
          . . . x . x . . . . . . x . x . . .
          . . . . x . . . . . . . . x . . . .
          . . . x . x . . . . . . x . x . . .
          . . x . . . x . . . . x . . . x . .
          . x . . . . . x . . x . . . . . x .
          x . . . . . . . x x . . . . . . . x
    HERE
    expected_output.gsub!(/^\s+/, '')
    assert_equal(expected_output, grid.to_s)
  end
end

class TestKnotworkPanel < Test::Unit::TestCase
end

There, that's another fifteen minutes of coding done. Yes, the combination of TDD and Ruby makes it about this easy to write a program.