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?
