Fix for JRUBY-2882. Handle error messages related to constructors better
[jruby.git] / bench / shootout / meteor.ruby-2.ruby
blob016271129b27564b9f1715b6e5b71091e1f96078
1 #!/usr/bin/env ruby
3 # The Computer Language Shootout
4 # http://shootout.alioth.debian.org
5 # contributed by Kevin Barnes (Ruby novice)
7 # PROGRAM: the main body is at the bottom.
8 # 1) read about the problem here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/
9 # 2) see how I represent a board as a bitmask by reading the blank_board comments
10 # 3) read as your mental paths take you
12 # class to represent all information about a particular rotation of a particular piece
13 class Rotation
14 # an array (by location) containing a bit mask for how the piece maps at the given location.
15 # if the rotation is illegal at that location the mask will contain false
16 attr_reader :start_masks
18 # maps a direction to a relative location. these differ depending on whether it is an even or
19 # odd row being mapped from
20 @@rotation_even_adder = { :west => -1, :east => 1, :nw => -7, :ne => -6, :sw => 5, :se => 6 }
21 @@rotation_odd_adder = { :west => -1, :east => 1, :nw => -6, :ne => -5, :sw => 6, :se => 7 }
23 def initialize( directions )
24 @even_offsets, @odd_offsets = normalize_offsets( get_values( directions ))
26 @even_mask = mask_for_offsets( @even_offsets)
27 @odd_mask = mask_for_offsets( @odd_offsets)
29 @start_masks = Array.new(60)
31 # create the rotational masks by placing the base mask at the location and seeing if
32 # 1) it overlaps the boundries and 2) it produces a prunable board. if either of these
33 # is true the piece cannot be placed
34 0.upto(59) do | offset |
35 mask = is_even(offset) ? (@even_mask << offset) : (@odd_mask << offset)
36 if (blank_board & mask == 0 && !prunable(blank_board | mask, 0, true)) then
37 imask = compute_required( mask, offset)
38 @start_masks[offset] = [ mask, imask, imask | mask ]
39 else
40 @start_masks[offset] = false
41 end
42 end
43 end
45 def compute_required( mask, offset )
46 board = blank_board
47 0.upto(offset) { | i | board |= 1 << i }
48 board |= mask
49 return 0 if (!prunable(board | mask, offset))
50 board = flood_fill(board,58)
51 count = 0
52 imask = 0
53 0.upto(59) do | i |
54 if (board[i] == 0) then
55 imask |= (1 << i)
56 count += 1
57 end
58 end
59 (count > 0 && count < 5) ? imask : 0
60 end
62 def flood_fill( board, location)
63 return board if (board[location] == 1)
64 board |= 1 << location
65 row, col = location.divmod(6)
66 board = flood_fill( board, location - 1) if (col > 0)
67 board = flood_fill( board, location + 1) if (col < 4)
68 if (row % 2 == 0) then
69 board = flood_fill( board, location - 7) if (col > 0 && row > 0)
70 board = flood_fill( board, location - 6) if (row > 0)
71 board = flood_fill( board, location + 6) if (row < 9)
72 board = flood_fill( board, location + 5) if (col > 0 && row < 9)
73 else
74 board = flood_fill( board, location - 5) if (col < 4 && row > 0)
75 board = flood_fill( board, location - 6) if (row > 0)
76 board = flood_fill( board, location + 6) if (row < 9)
77 board = flood_fill( board, location + 7) if (col < 4 && row < 9)
78 end
79 board
80 end
82 # given a location, produces a list of relative locations covered by the piece at this rotation
83 def offsets( location)
84 if is_even( location) then
85 @even_offsets.collect { | value | value + location }
86 else
87 @odd_offsets.collect { | value | value + location }
88 end
89 end
91 # returns a set of offsets relative to the top-left most piece of the rotation (by even or odd rows)
92 # this is hard to explain. imagine we have this partial board:
93 # 0 0 0 0 0 x [positions 0-5]
94 # 0 0 1 1 0 x [positions 6-11]
95 # 0 0 1 0 0 x [positions 12-17]
96 # 0 1 0 0 0 x [positions 18-23]
97 # 0 1 0 0 0 x [positions 24-29]
98 # 0 0 0 0 0 x [positions 30-35]
99 # ...
100 # The top-left of the piece is at position 8, the
101 # board would be passed as a set of positions (values array) containing [8,9,14,19,25] not necessarily in that
102 # sorted order. Since that array starts on an odd row, the offsets for an odd row are: [0,1,6,11,17] obtained
103 # by subtracting 8 from everything. Now imagine the piece shifted up and to the right so it's on an even row:
104 # 0 0 0 1 1 x [positions 0-5]
105 # 0 0 1 0 0 x [positions 6-11]
106 # 0 0 1 0 0 x [positions 12-17]
107 # 0 1 0 0 0 x [positions 18-23]
108 # 0 0 0 0 0 x [positions 24-29]
109 # 0 0 0 0 0 x [positions 30-35]
110 # ...
111 # Now the positions are [3,4,8,14,19] which after subtracting the lowest value (3) gives [0,1,5,11,16] thus, the
112 # offsets for this particular piece are (in even, odd order) [0,1,5,11,16],[0,1,6,11,17] which is what
113 # this function would return
114 def normalize_offsets( values)
115 min = values.min
116 even_min = is_even(min)
117 other_min = even_min ? min + 6 : min + 7
118 other_values = values.collect do | value |
119 if is_even(value) then
120 value + 6 - other_min
121 else
122 value + 7 - other_min
125 values.collect! { | value | value - min }
127 if even_min then
128 [values, other_values]
129 else
130 [other_values, values]
134 # produce a bitmask representation of an array of offset locations
135 def mask_for_offsets( offsets )
136 mask = 0
137 offsets.each { | value | mask = mask + ( 1 << value ) }
138 mask
141 # finds a "safe" position that a position as described by a list of directions can be placed
142 # without falling off any edge of the board. the values returned a location to place the first piece
143 # at so it will fit after making the described moves
144 def start_adjust( directions )
145 south = east = 0;
146 directions.each do | direction |
147 east += 1 if ( direction == :sw || direction == :nw || direction == :west )
148 south += 1 if ( direction == :nw || direction == :ne )
150 south * 6 + east
153 # given a set of directions places the piece (as defined by a set of directions) on the board at
154 # a location that will not take it off the edge
155 def get_values ( directions )
156 start = start_adjust(directions)
157 values = [ start ]
158 directions.each do | direction |
159 if (start % 12 >= 6) then
160 start += @@rotation_odd_adder[direction]
161 else
162 start += @@rotation_even_adder[direction]
164 values += [ start ]
167 # some moves take you back to an existing location, we'll strip duplicates
168 values.uniq
172 # describes a piece and caches information about its rotations to as to be efficient for iteration
173 # ATTRIBUTES:
174 # rotations -- all the rotations of the piece
175 # type -- a numeic "name" of the piece
176 # masks -- an array by location of all legal rotational masks (a n inner array) for that location
177 # placed -- the mask that this piece was last placed at (not a location, but the actual mask used)
178 class Piece
179 attr_reader :rotations, :type, :masks
180 attr_accessor :placed
182 # transform hashes that change one direction into another when you either flip or rotate a set of directions
183 @@flip_converter = { :west => :west, :east => :east, :nw => :sw, :ne => :se, :sw => :nw, :se => :ne }
184 @@rotate_converter = { :west => :nw, :east => :se, :nw => :ne, :ne => :east, :sw => :west, :se => :sw }
186 def initialize( directions, type )
187 @type = type
188 @rotations = Array.new();
189 @map = {}
191 generate_rotations( directions )
192 directions.collect! { | value | @@flip_converter[value] }
193 generate_rotations( directions )
195 # creates the masks AND a map that returns [location, rotation] for any given mask
196 # this is used when a board is found and we want to draw it, otherwise the map is unused
197 @masks = Array.new();
198 0.upto(59) do | i |
199 even = true
200 @masks[i] = @rotations.collect do | rotation |
201 mask = rotation.start_masks[i]
202 @map[mask[0]] = [ i, rotation ] if (mask)
203 mask || nil
205 @masks[i].compact!
209 # rotates a set of directions through all six angles and adds a Rotation to the list for each one
210 def generate_rotations( directions )
211 6.times do
212 rotations.push( Rotation.new(directions))
213 directions.collect! { | value | @@rotate_converter[value] }
217 # given a board string, adds this piece to the board at whatever location/rotation
218 # important: the outbound board string is 5 wide, the normal location notation is six wide (padded)
219 def fill_string( board_string)
220 location, rotation = @map[@placed]
221 rotation.offsets(location).each do | offset |
222 row, col = offset.divmod(6)
223 board_string[ row*5 + col, 1 ] = @type.to_s
228 # a blank bit board having this form:
230 # 0 0 0 0 0 1
231 # 0 0 0 0 0 1
232 # 0 0 0 0 0 1
233 # 0 0 0 0 0 1
234 # 0 0 0 0 0 1
235 # 0 0 0 0 0 1
236 # 0 0 0 0 0 1
237 # 0 0 0 0 0 1
238 # 0 0 0 0 0 1
239 # 0 0 0 0 0 1
240 # 1 1 1 1 1 1
242 # where left lest significant bit is the top left and the most significant is the lower right
243 # the actual board only consists of the 0 places, the 1 places are blockers to keep things from running
244 # off the edges or bottom
245 def blank_board
246 0b111111100000100000100000100000100000100000100000100000100000100000
249 def full_board
250 0b111111111111111111111111111111111111111111111111111111111111111111
253 # determines if a location (bit position) is in an even row
254 def is_even( location)
255 (location % 12) < 6
258 # support function that create three utility maps:
259 # @@converter -- for each row an array that maps a five bit row (via array mapping)
260 # to the a a five bit representation of the bits below it
261 # @@bit_count -- maps a five bit row (via array mapping) to the number of 1s in the row
262 # @@new_regions -- maps a five bit row (via array mapping) to an array of "region" arrays
263 # a region array has three values the first is a mask of bits in the region,
264 # the second is the count of those bits and the third is identical to the first
265 # examples:
266 # 0b10010 => [ 0b01100, 2, 0b01100 ], [ 0b00001, 1, 0b00001]
267 # 0b01010 => [ 0b10000, 1, 0b10000 ], [ 0b00100, 1, 0b00100 ], [ 0b00001, 1, 0b00001]
268 # 0b10001 => [ 0b01110, 3, 0b01110 ]
269 def create_collector_support
270 odd_map = [0b11, 0b110, 0b1100, 0b11000, 0b10000]
271 even_map = [0b1, 0b11, 0b110, 0b1100, 0b11000]
273 all_odds = Array.new(0b100000)
274 all_evens = Array.new(0b100000)
275 bit_counts = Array.new(0b100000)
276 new_regions = Array.new(0b100000)
277 0.upto(0b11111) do | i |
278 bit_count = odd = even = 0
279 0.upto(4) do | bit |
280 if (i[bit] == 1) then
281 bit_count += 1
282 odd |= odd_map[bit]
283 even |= even_map[bit]
286 all_odds[i] = odd
287 all_evens[i] = even
288 bit_counts[i] = bit_count
289 new_regions[i] = create_regions( i)
292 @@converter = []
293 10.times { | row | @@converter.push((row % 2 == 0) ? all_evens : all_odds) }
294 @@bit_counts = bit_counts
295 @@regions = new_regions.collect { | set | set.collect { | value | [ value, bit_counts[value], value] } }
298 # determines if a board is punable, meaning that there is no possibility that it
299 # can be filled up with pieces. A board is prunable if there is a grouping of unfilled spaces
300 # that are not a multiple of five. The following board is an example of a prunable board:
301 # 0 0 1 0 0
302 # 0 1 0 0 0
303 # 1 1 0 0 0
304 # 0 1 0 0 0
305 # 0 0 0 0 0
306 # ...
308 # This board is prunable because the top left corner is only 3 bits in area, no piece will ever fit it
309 # parameters:
310 # board -- an initial bit board (6 bit padded rows, see blank_board for format)
311 # location -- starting location, everything above and to the left is already full
312 # slotting -- set to true only when testing initial pieces, when filling normally
313 # additional assumptions are possible
315 # Algorithm:
316 # The algorithm starts at the top row (as determined by location) and iterates a row at a time
317 # maintainng counts of active open areas (kept in the collector array) each collector contains
318 # three values at the start of an iteration:
319 # 0: mask of bits that would be adjacent to the collector in this row
320 # 1: the number of bits collected so far
321 # 2: a scratch space starting as zero, but used during the computation to represent
322 # the empty bits in the new row that are adjacent (position 0)
323 # The exact procedure is described in-code
324 def prunable( board, location, slotting = false)
325 collectors = []
326 # loop accross the rows
327 (location / 6).to_i.upto(9) do | row_on |
328 # obtain a set of regions representing the bits of the curent row.
329 regions = @@regions[(board >> (row_on * 6)) & 0b11111]
330 converter = @@converter[row_on]
332 # track the number of collectors at the start of the cycle so that
333 # we don't compute against newly created collectors, only existing collectors
334 initial_collector_count = collectors.length
336 # loop against the regions. For each region of the row
337 # we will see if it connects to one or more existing collectors.
338 # if it connects to 1 collector, the bits from the region are added to the
339 # bits of the collector and the mask is placed in collector[2]
340 # If the region overlaps more than one collector then all the collectors
341 # it overlaps with are merged into the first one (the others are set to nil in the array)
342 # if NO collectors are found then the region is copied as a new collector
343 regions.each do | region |
344 collector_found = nil
345 region_mask = region[2]
346 initial_collector_count.times do | collector_num |
347 collector = collectors[collector_num]
348 if (collector) then
349 collector_mask = collector[0]
350 if (collector_mask & region_mask != 0) then
351 if (collector_found) then
352 collector_found[0] |= collector_mask
353 collector_found[1] += collector[1]
354 collector_found[2] |= collector[2]
355 collectors[collector_num] = nil
356 else
357 collector_found = collector
358 collector[1] += region[1]
359 collector[2] |= region_mask
364 if (collector_found == nil) then
365 collectors.push(Array.new(region))
369 # check the existing collectors, if any collector overlapped no bits in the region its [2] value will
370 # be zero. The size of any such reaason is tested if it is not a muliple of five true is returned since
371 # the board is prunable. if it is a multiple of five it is removed.
372 # Collector that are still active have a new adjacent value [0] set based n the matched bits
373 # and have [2] cleared out for the next cycle.
374 collectors.length.times do | collector_num |
375 collector = collectors[collector_num]
376 if (collector) then
377 if (collector[2] == 0) then
378 return true if (collector[1] % 5 != 0)
379 collectors[collector_num] = nil
380 else
381 # if a collector matches all bits in the row then we can return unprunable early for the
382 # follwing reasons:
383 # 1) there can be no more unavailable bits bince we fill from the top left downward
384 # 2) all previous regions have been closed or joined so only this region can fail
385 # 3) this region must be good since there can never be only 1 region that is nuot
386 # a multiple of five
387 # this rule only applies when filling normally, so we ignore the rule if we are "slotting"
388 # in pieces to see what configurations work for them (the only other time this algorithm is used).
389 return false if (collector[2] == 0b11111 && !slotting)
390 collector[0] = converter[collector[2]]
391 collector[2] = 0
396 # get rid of all the empty converters for the next round
397 collectors.compact!
399 return false if (collectors.length <= 1) # 1 collector or less and the region is fine
400 collectors.any? { | collector | (collector[1] % 5) != 0 } # more than 1 and we test them all for bad size
403 # creates a region given a row mask. see prunable for what a "region" is
404 def create_regions( value )
405 regions = []
406 cur_region = 0
407 5.times do | bit |
408 if (value[bit] == 0) then
409 cur_region |= 1 << bit
410 else
411 if (cur_region != 0 ) then
412 regions.push( cur_region)
413 cur_region = 0;
417 regions.push(cur_region) if (cur_region != 0)
418 regions
421 # find up to the counted number of solutions (or all solutions) and prints the final result
422 def find_all
423 find_top( 1)
424 find_top( 0)
425 print_results
428 # show the board
429 def print_results
430 print "#{@boards_found} solutions found\n\n"
431 print_full_board( @min_board)
432 print "\n"
433 print_full_board( @max_board)
434 print "\n"
437 # finds solutions. This special version of the main function is only used for the top level
438 # the reason for it is basically to force a particular ordering on how the rotations are tested for
439 # the first piece. It is called twice, first looking for placements of the odd rotations and then
440 # looking for placements of the even locations.
442 # WHY?
443 # Since any found solution has an inverse we want to maximize finding solutions that are not already found
444 # as an inverse. The inverse will ALWAYS be 3 one of the piece configurations that is exactly 3 rotations away
445 # (an odd number). Checking even vs odd then produces a higher probability of finding more pieces earlier
446 # in the cycle. We still need to keep checking all the permutations, but our probability of finding one will
447 # diminsh over time. Since we are TOLD how many to search for this lets us exit before checking all pieces
448 # this bennifit is very great when seeking small numbers of solutions and is 0 when looking for more than the
449 # maximum number
450 def find_top( rotation_skip)
451 board = blank_board
452 (@pieces.length-1).times do
453 piece = @pieces.shift
454 piece.masks[0].each do | mask, imask, cmask |
455 if ((rotation_skip += 1) % 2 == 0) then
456 piece.placed = mask
457 find( 1, 1, board | mask)
460 @pieces.push(piece)
462 piece = @pieces.shift
463 @pieces.push(piece)
466 # the normail find routine, iterates through the available pieces, checks all rotations at the current location
467 # and adds any boards found. depth is acheived via recursion. the overall approach is described
468 # here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/
469 # parameters:
470 # start_location -- where to start looking for place for the next piece at
471 # placed -- number of pieces placed
472 # board -- current state of the board
474 # see in-code comments
475 def find( start_location, placed, board)
476 # find the next location to place a piece by looking for an empty bit
477 while board[start_location] == 1
478 start_location += 1
481 @pieces.length.times do
482 piece = @pieces.shift
483 piece.masks[start_location].each do | mask, imask, cmask |
484 if ( board & cmask == imask) then
485 piece.placed = mask
486 if (placed == 9) then
487 add_board
488 else
489 find( start_location + 1, placed + 1, board | mask)
493 @pieces.push(piece)
497 # print the board
498 def print_full_board( board_string)
499 10.times do | row |
500 print " " if (row % 2 == 1)
501 5.times do | col |
502 print "#{board_string[row*5 + col,1]} "
504 print "\n"
508 # when a board is found we "draw it" into a string and then flip that string, adding both to
509 # the list (hash) of solutions if they are unique.
510 def add_board
511 board_string = "99999999999999999999999999999999999999999999999999"
512 @all_pieces.each { | piece | piece.fill_string( board_string ) }
513 save( board_string)
514 save( board_string.reverse)
517 # adds a board string to the list (if new) and updates the current best/worst board
518 def save( board_string)
519 if (@all_boards[board_string] == nil) then
520 @min_board = board_string if (board_string < @min_board)
521 @max_board = board_string if (board_string > @max_board)
522 @all_boards.store(board_string,true)
523 @boards_found += 1
525 # the exit motif is a time saver. Ideally the function should return, but those tests
526 # take noticable time (performance).
527 if (@boards_found == @stop_count) then
528 print_results
529 exit(0)
536 ## MAIN BODY :)
538 create_collector_support
539 @pieces = [
540 Piece.new( [ :nw, :ne, :east, :east ], 2),
541 Piece.new( [ :ne, :se, :east, :ne ], 7),
542 Piece.new( [ :ne, :east, :ne, :nw ], 1),
543 Piece.new( [ :east, :sw, :sw, :se ], 6),
544 Piece.new( [ :east, :ne, :se, :ne ], 5),
545 Piece.new( [ :east, :east, :east, :se ], 0),
546 Piece.new( [ :ne, :nw, :se, :east, :se ], 4),
547 Piece.new( [ :se, :se, :se, :west ], 9),
548 Piece.new( [ :se, :se, :east, :se ], 8),
549 Piece.new( [ :east, :east, :sw, :se ], 3)
552 @all_pieces = Array.new( @pieces)
554 @min_board = "99999999999999999999999999999999999999999999999999"
555 @max_board = "00000000000000000000000000000000000000000000000000"
556 @stop_count = ARGV[0].to_i || 2089
557 @all_boards = {}
558 @boards_found = 0
560 find_all ######## DO IT!!!