3
\$\begingroup\$

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
  attr_reader :size, :snakes, :ladders

  def initialize(size, snakes = 10, ladders = 10)
    @size = size
    @snakes, @ladders = {}, {}
    place_snakes(snakes)
    place_ladders(ladders)
  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

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

      ladders[start_point] = place_point(end_point_range)
    end
  end

private

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

  def occupied?(number)
    [snakes.keys, snakes.values, ladders.keys, ladders.values].flatten.include?(number)
  end

  def habitable_range
    1..(size - 1)
  end
end
\$\endgroup\$
4
  • \$\begingroup\$ 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. :/ \$\endgroup\$ – Mohamad Nov 20 '14 at 20:33
  • \$\begingroup\$ 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. \$\endgroup\$ – Chris Heald Nov 20 '14 at 20:41
  • \$\begingroup\$ 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. \$\endgroup\$ – Mohamad Nov 20 '14 at 20:46
  • \$\begingroup\$ Just enforce that start_point != end_point when generating a point pair. \$\endgroup\$ – Chris Heald Nov 20 '14 at 20:46
2
\$\begingroup\$

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

class World
  attr_reader :size, :snakes, :ladders

   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]]
    @ladders = Hash[pairs[snakes, ladders]]
  end
end
\$\endgroup\$
1
  • \$\begingroup\$ 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. \$\endgroup\$ – Mohamad Nov 21 '14 at 15:02
3
\$\begingroup\$

Here's an attempt:

class World
  attr_reader :board

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

    place snakes_count,  :snake
    place ladders_count, :ladder
  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.

\$\endgroup\$
3
  • \$\begingroup\$ 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. \$\endgroup\$ – Mohamad Nov 20 '14 at 21:33
  • \$\begingroup\$ Isn't ... && end_point != start_point redundant? free_point will never return an index that is already occupied. \$\endgroup\$ – Mohamad Nov 20 '14 at 23:20
  • \$\begingroup\$ 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. \$\endgroup\$ – Chris Heald Nov 20 '14 at 23:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.