Snakes and Ladders random world generator second implementation

The following is an implementation of a random world generator for the classic Snakes and Ladders game. This is a follow up to this question, where I reviewed my own original implementation. I am asking for a review of my review.

The world rules are:

1. The head of a snake can not be on the last square (you'll never win)
2. The base of a ladder can not be on the last square (you'll have no where to go)
3. A square can only have one thing, either the head or base of a ladder or a snake
class World

def initialize(size, snakes = 10, ladders = 10)
@size = size
place_snakes(snakes)
end

def place_snakes(count)
count.times do
start_point = place_point
end_point_range = habitable_range.begin..(start_point - 1)

snakes[start_point] = place_point(end_point_range)
end
end

count.times do
start_point = place_point
end_point_range = (start_point + 1)..habitable_range.end

end
end

private

def place_point(range = habitable_range)
begin
point = rand(range)
end while occupied?(point)
point
end

def occupied?(number)
end

def habitable_range
1..(size - 1)
end
end

• Despite my best efforts this code has two serious bugs. If a snake's start_point = 1 then place_point returns nil when placing a snake's end point. So habitable_range.begin..(start_point - 1) becomes 1..(1-1) which is 1..0, and place_point(1..0) == nil. The same is true for ladders if the ladder's start point is 99. :/ – Mohamad Nov 20 '14 at 20:33
• You don't need to enforce different ranges for snakes/latters; a snake is simply a point pair which cannot have the final point and is ordered descending, and a ladder is simply a point pair which is ordered ascending. That can help you simplify the code a lot. – Chris Heald Nov 20 '14 at 20:41
• But how would you ensure that a snake will not have a head on 5 and a tail on 5? That would make sense. The distance between a snakes head and tail must be -1 minimum. – Mohamad Nov 20 '14 at 20:46
• Just enforce that start_point != end_point when generating a point pair. – Chris Heald Nov 20 '14 at 20:46

Instead of doing an iterative process, why don't you get the hashes from a one-time shuffling of the pair positions?

class World

def initialize(size, snakes = 10, ladders = 10)
@size = size
pairs = (0...size).to_a.shuffle.each_slice(2).map(&:sort)
@snakes = Hash[pairs[0, snakes]]
end
end

• I was about to post my own review with a very similar approach, where I generate the number pairs once, and then reverse the snake arrays. I will still post it just for reference. I also believe I should split the board into two classes: the Board, and a generator class that just makes the pairs. – Mohamad Nov 21 '14 at 15:02

Here's an attempt:

class World

def initialize(size, snakes_count = 10, ladders_count = 10)
@board = Array.new(size * size)

place snakes_count,  :snake
end

private

def free_point
point = rand(@board.size) until point && @board[point].nil?
point
end

def place(count, entity)
count.times do
start_point = free_point
end_point   = free_point until end_point && end_point != start_point && (entity == :ladder || end_point != @board.size - 1)
pair = [start_point, end_point].sort
pair.reverse! if entity == :snake
@board[pair.first] = @board[pair.last] = pair
end
end
end


The idea here is that:

1. A snake or ladder consists of a point pair. A snake is a desending pair (ie, go back from a higher point to a lower point), while a ladder is an ascending pair (go forward from a lower point to a higher point)
2. A snake cannot have either of its points be the final point on the board.

The board is represented as a size x size array, so a world of size 12 would produce a board that is 12x12, or 144 squares. Checking for occupation is then as easy as just checking if @board[point].nil?. It also gives you an easy way to iterate the board for rendering or rules checking; as you're playing, if the player lands on square 63, you can check square 63. If it has a value, then check if v[0] > v[1] (snake) or otherwise (ladder).

If desired, you could have #place keep a separate list of snakes and ladders, but it's not strictly necessary, since you can think of them as the same construct (a movement from one point to another), differentiated only by the direction of movement.

• Thanks. The issue that both snakes and ladders is so similar except for the order was bugging from the start. But my thoughts got stuck between the walls that my class implementation constructed. I built a box for myself that I couldn't think outside of. I would still simplify this as I think so parts are not as readable, but I see the point. – Mohamad Nov 20 '14 at 21:33
• Isn't ... && end_point != start_point redundant? free_point will never return an index that is already occupied. – Mohamad Nov 20 '14 at 23:20
• No, because free_point doesn't consume the point in @board. That doesn't happen until after both start/end points have been generated, so we need to explicitly check for a collision. – Chris Heald Nov 20 '14 at 23:36