Skip to content

Commit

Permalink
Initial commit: complete Rush Hour
Browse files Browse the repository at this point in the history
  • Loading branch information
petertseng committed Dec 2, 2016
0 parents commit 095743b
Show file tree
Hide file tree
Showing 7 changed files with 419 additions and 0 deletions.
1 change: 1 addition & 0 deletions .travis.yml
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Name

[![Build Status](https://travis-ci.org/petertseng-dp/rushhour.svg?branch=master)](https://travis-ci.org/petertseng-dp/rushhour)

# Notes

I used a similar solution as a Reddit poster - represent the board as a bitfield.
The maximum supported board size is 6x6, since the borders are all set to 1 to make collision detection easier.
Car orientation is set in the low-order bits (which are usually for the border).

Confession: This was originally written in Ruby and then converted to Crystal.

# Source

https://www.reddit.com/r/dailyprogrammer/comments/56bh88
26 changes: 26 additions & 0 deletions rushhour.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "./src/rushhour"

INPUTS = [
"
..ABBC
..A..C
..ARRC>
...EFF
GHHE..
G..EII
",
].map(&.strip)

INPUTS.each { |input|
start = Time.now
cars = parse_cars(input)
soln = solve(cars).not_nil!
puts input
chunk_moves(soln).each_with_index { |(move, n), i|
car, dir = move
puts "%2d. %s %5s %d" % [i + 1, car, dir, n]
}
puts "Cars moved a total of #{soln.size} spaces"
puts "Solved in #{Time.now - start}"
puts
}
169 changes: 169 additions & 0 deletions rushhour.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
require 'set'

# Include the edges in these numbers.
# Constraint: With 64-bit integers, WIDTH * HEIGHT <= 64
WIDTH = 8
HEIGHT = 8

# Assumption: These squares are edges so it's OK to reuse these bits.
VERTICAL = 1
HORIZONTAL = 2

CAR_ORIENTATION = VERTICAL | HORIZONTAL
CAR_POSITION = -1 & ~CAR_ORIENTATION

def square(row:, col:)
1 << (row * HEIGHT + col)
end

ESCAPE = square(row: 3, col: WIDTH - 1)

EDGE = (0...(WIDTH * HEIGHT)).select { |n|
col = n % WIDTH
row = n / WIDTH
col == 0 || row == 0 || col == (WIDTH - 1) || row == (HEIGHT - 1)
}.map { |n| 1 << n }.reduce(:|) & ~ESCAPE
raise "bad edge #{EDGE.to_s(16)}" unless EDGE.to_s(16) == 'ff818181018181ff'

ROWS = (0...HEIGHT).map { |row| (0...WIDTH).map { |col| square(row: row, col: col) }.reduce(:|) }
COLS = (0...WIDTH).map { |col| (0...HEIGHT).map { |row| square(row: row, col: col) }.reduce(:|) }

def parse_cars(grid)
cars = Hash.new(0)
grid.each_line.with_index { |line, row|
line.chomp.each_char.with_index { |c, col|
next if c == '>' || c == '.'
cars[c] |= square(row: row + 1, col: col + 1)
}
}

cars.each { |car, squares|
horiz_car, vert_car = [ROWS, COLS].map { |lines|
lines.count { |line| squares & line != 0 } == 1
}
raise "#{car} can't be both vertical and horizontal" if horiz_car && vert_car
raise "#{car} can't be neither vertical nor horizontal" if !horiz_car && !vert_car
cars[car] |= HORIZONTAL if horiz_car
cars[car] |= VERTICAL if vert_car
}

cars
end

def solve(initial_cars)
# prev[current_board] = [cars, car, dir]
prev = {initial_cars => nil}
queue = [initial_cars]
while (current_cars = queue.shift)
# Could mask out the orientation, but it doesn't matter
# because the orientation is an edge
board = current_cars.values.reduce(EDGE, :|)
current_cars.each { |car, squares_and_orientation|
squares = squares_and_orientation & CAR_POSITION
board_without_car = board & ~squares
[
[HORIZONTAL, ->(s) { s << 1 }, :right],
[HORIZONTAL, ->(s) { s >> 1 }, :left],
[VERTICAL, ->(s) { s << WIDTH }, :down],
[VERTICAL, ->(s) { s >> WIDTH }, :up],
].each { |orientation, move_f, move_dir|
next if squares_and_orientation & orientation == 0
new_position = move_f[squares]
next unless board_without_car & new_position == 0

if new_position & ESCAPE != 0
trace_current_cars = current_cars
moves = [[car, move_dir]]
while (prev_move = prev[trace_current_cars])
prev_cars, prev_move_car, prev_move_dir = prev_move
moves << [prev_move_car, prev_move_dir]
trace_current_cars = prev_cars
end
return moves.reverse
end

# If cars ever have more than one orientation, it will be lost at the below line.
new_cars = current_cars.merge(car => new_position | orientation)
next if prev.has_key?(new_cars)
queue << new_cars
prev[new_cars] = [current_cars, car, move_dir]
}
}
end
end

def chunk_moves(moves)
prev_move = nil
moves.each_with_object([]) { |move, chunked|
if move == prev_move
chunked[-1][-1] += 1
else
chunked << move + [1]
prev_move = move
end
}
end

INPUTS = ['
......
......
RR....>
......
......
......
','
..A...
..A...
RRA...>
......
......
......
','
GAA..Y
G.V..Y
RRV..Y>
..VZZZ
....B.
WWW.B.
','
.....Y
.....Y
...RRY>
...ZZZ
......
...WWW
','
TTTAU.
...AU.
RR..UB>
CDDFFB
CEEG.H
VVVG.H
','
QQQWEU
TYYWEU
T.RREU>
IIO...
.PO.AA
.PSSDD
','
..ABBC
..A..C
..ARRC>
...EFF
GHHE..
G..EII
'].map(&:strip)

INPUTS.each { |input|
start = Time.now
cars = parse_cars(input)
soln = solve(cars)
puts input
chunk_moves(soln).each_with_index { |(car, dir, n), i|
puts '%2d. %s %5s %d' % [i + 1, car, dir, n]
}
puts "Cars moved a total of #{soln.size} spaces"
puts "Solved in #{Time.now - start}"
puts
}
98 changes: 98 additions & 0 deletions spec/rushhour_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
require "spec"
require "../src/rushhour"

inputs = [
{
"
......
......
RR....>
......
......
......
",
5,
},
{
"
..A...
..A...
RRA...>
......
......
......
",
8,
},
{
# I originally cared about this case because my code would move Z, Y, W, Y
# and I wanted to see if I could get it to move Z, W, Y instead.
# It's the same number of single-square moves, but fewer cars move.
# The number of single-square moves should still be the primary to optimise for.
# The number of distinct cars moved would have been the secondary.
# I did not implement this secondary.
"
.....Y
.....Y
...RRY>
...ZZZ
......
...WWW
",
7,
},
{
"
GAA..Y
G.V..Y
RRV..Y>
..VZZZ
....B.
WWW.B.
",
34,
},
{
"
TTTAU.
...AU.
RR..UB>
CDDFFB
CEEG.H
VVVG.H
",
14,
},
{
"
QQQWEU
TYYWEU
T.RREU>
IIO...
.PO.AA
.PSSDD
",
94,
},
{
"
..ABBC
..A..C
..ARRC>
...EFF
GHHE..
G..EII
",
84,
},
]

describe :solve do
it "solves" do
inputs.each { |board, moves|
cars = parse_cars(board.strip)
soln = solve(cars).not_nil!
soln.size.should eq(moves)
}
end
end
Loading

0 comments on commit 095743b

Please sign in to comment.