COOLNAMEHERE

Drawing Celtic Knotwork 4

Things get a little more complex now, because we're on to creating a KnotworkPanel. Every KnotworkPanel uses a specific set of predefined tiles, (thanks again to Andy Sloss for going to the trouble of defining them!), which I will store as class variables. I would probably store them in a different file if I wanted to include every Tile defined in Sloss's book, but that's more than I want to chew on today.

Certain Tiles must go in certain locations of the KnotworkPanel's Grid, such as the corners and edges, and we have to remember this in our tests. Ah, let's just cut and paste the source strings and squish them together so that they look like what we're aiming for. That's probably the easiest test for now.

For a while, my problem was that I was copying and pasting incorrectly. Don't ask me how I pull this stuff off. I'm just special, I guess. The test results would end up spewing out "Expected "... a really long chain of x and . characters, " but got " ... a really long chain of x and . characters, almost identical to the first chain. My solution? Test each row of output, that way I see exactly which line was one character off.

# 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)
    if row >= @rows or column >= @columns then
      raise ArgumentError, \
      "set_tile at #\{row}, #\{column} outside of Grid area " \
      "(#\{@rows}, #\{@columns})"
    end
    
    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

# I am a lovely Celtic knotwork panel. I know my dimensions, and can output myself
# as ASCII art.
class KnotworkPanel
  
  @@top_left = Tile.new(%{. . . . x x x x x
                          . . . . x . . . .
                          . . . . x . . . .
                          . . . . x . . . x
                          . . . . x . . x .
                          . . . . x . . x .
                          . . . . . x . . x
                          . . . . . x . . x
                          . . . . . . x x .}.gsub(/^\s+/, ''))
      
  @@top      = Tile.new(%{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 .}.gsub(/^\s+/, '')
                        )
      
  @@topright = Tile.new(%{x x x x x . . . .
                          . . . . x . . . .
                          . . . . x . . . .
                          x . . . x . . . .
                          . x . . x . . . .
                          . x . . x . . . .
                          x . . x . . . . .
                          . . . x . . . . .
                          . . x . . . . . .}.gsub(/^\s+/, '')
                        )
      
  @@left     = Tile.new(%{. . . . . . x . .
                          . . . . . x . . .
                          . . . . . x . . .
                          . . . . x . . . x
                          . . . . x . . x .
                          . . . . x . . x .
                          . . . . . x . . x
                          . . . . . x . . x
                          . . . . . . x x .}.gsub(/^\s+/, '')
                            )
      
      @@center   = Tile.new(%{. . x . . . x . .
                              . x . x . x . . .
                              x . . . x . . . x
                              . x . . . x . x .
                              . . x . . . x . .
                              . x . x . . . x .
                              x . . . x . . . x
                              . . . x . x . x .
                              . . x . . . x . .}.gsub(/^\s+/, '')
                            )
      
      @@right    = Tile.new(%{. x x . . . . . .
                              x . . x . . . . .
                              x . . x . . . . .
                              . x . . x . . . .
                              . x . . x . . . .
                              . x . . x . . . .
                              x . . x . . . . .
                              . . . x . . . . .
                              . . x . . . . . .}.gsub(/^\s+/, '')
                            )
      
      @@bot_left = Tile.new(%{. . . . . . x . .
                              . . . . . x . . .
                              . . . . . x . . x
                              . . . . x . . x .
                              . . . . x . . x .
                              . . . . x . . . x
                              . . . . x . . . .
                              . . . . x . . . .
                              . . . . x x x x x}.gsub(/^\s+/, '')
                            )
  
  @@bottom   = Tile.new(%{. 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}.gsub(/^\s+/, '')
                        )
  
  @@botright = Tile.new(%{. x x . . . . . .
                          x . . x . . . . .
                          x . . x . . . . .
                          . x . . x . . . .
                          . x . . x . . . .
                          x . . . x . . . .
                          . . . . x . . . .
                          . . . . x . . . .
                          x x x x x . . . .}.gsub(/^\s+/, '')
                        )
      
  def initialize(rows, columns=rows)
    @row_size = rows + 2
    @col_size = columns + 2
    
    @grid = Grid.new(@row_size, @col_size)
    
    # Set the top row
    @grid.set_tile(0, 0, @@top_left)
    (1...@col_size-1).each do |i|
      @grid.set_tile(0, i, @@top)
    end
    @grid.set_tile(0, @col_size-1, @@topright)
    
    # Set the center rows.
    (1...@row_size-1).each do |i|
      @grid.set_tile(i, 0, @@left)
      (1...@col_size-1).each do |j|
        @grid.set_tile(i, j, @@center)
      end
      @grid.set_tile(i, @col_size-1, @@right)
    end
    
    # Set the bottom row
    @grid.set_tile(@row_size-1, 0, @@bot_left)
    (1...@col_size-1).each do |i|
      @grid.set_tile(@row_size-1, i, @@bottom)
    end
    @grid.set_tile(@row_size-1, @col_size-1, @@botright)
  end
  
  def to_aa()
    return @grid.to_s
  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)
    assert_raise(ArgumentError, "You cannot set a Tile outside of the Grid.") \
    { grid.set_tile(1, 1, tile) }
  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
  def test_ascii
    panel = KnotworkPanel.new(1)
    ascii_output_1 =<<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 . . . 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 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 . . 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 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 x x x x x x . . . . . x x x x x x x . . . .
HERE
    
    ascii_output_1.gsub!(/^\s+/, '')
    real_output = panel.to_aa.split("\n")
    ascii_output_1.split("\n").each_with_index do |line, i|
      assert_equal(line, real_output[i],
                   "line #{i} doesn't match")
    end
    
  end
  
  def test_large_panels
    ascii_output_1 =<<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 . . . 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 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 . . 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 . . . . .
        . . . . 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 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 . . . 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
        
    ascii_output_1.gsub!(/^\s+/, '')
    panel = KnotworkPanel.new(2, 1)
    real_output = panel.to_aa.split("\n")
    ascii_output_1.split("\n").each_with_index do |line, i|
      assert_equal(line, real_output[i],
                   "line #{i} doesn't match")
    end
  end
  
end


Yeah, that works. Let's move on.

Wait. This is me from the future, editing this page. KnotworkPanel's initializer kinda bugs me. It's fine for what it does, but how's the average person supposed to know that KnotworkPanel.new(1,2) actually creates a KnotworkPanel with dimensions of 3×5, including the border? Really, this is why you need some person to represent the customer or a user besides yourself whenever you want to write code for other people to use. I could change the initializer, but I really want to get the article out so the geeks of the world can point and laugh at my mistakes. For now, just remember that the dimensions sent to the initializer don't include the bordering tiles. So if you want a 5×5 panel, you'll have to call KnotworkPanel.new(3, 3). Man, that's ugly. Let's make fixing that an exercise at the end of the article, okay?

Copyright 1999 - 2008 Brian Wisti

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 United States License.