Skip to main content
We’ve updated our Terms of Service. A new AI Addendum clarifies how Stack Overflow utilizes AI interactions.
replaced http://codereview.stackexchange.com/ with https://codereview.stackexchange.com/
Source Link

I'm not a beginner to programming, but I am to programming in OOP (effectively at least). So to learn better I wrote an object oriented game of Snakes and Ladders. For background, see this questionthis question.

I'm not a beginner to programming, but I am to programming in OOP (effectively at least). So to learn better I wrote an object oriented game of Snakes and Ladders. For background, see this question.

I'm not a beginner to programming, but I am to programming in OOP (effectively at least). So to learn better I wrote an object oriented game of Snakes and Ladders. For background, see this question.

Tweeted twitter.com/#!/StackCodeReview/status/537941843615707136
edited title
Link
Mohamad
  • 2k
  • 16
  • 30

Object Ruby object oriented Snakes and Ladders implementation

Source Link
Mohamad
  • 2k
  • 16
  • 30

Object oriented Snakes and Ladders implementation

I'm not a beginner to programming, but I am to programming in OOP (effectively at least). So to learn better I wrote an object oriented game of Snakes and Ladders. For background, see this question.

I wrote four classes: Board, Cell, Portal, and Player. All are self explanatory except for Portal. A Portal object represents a snake or a ladder cell. Portals are just cells with behaviour. Their behaviour is identical save for the direction they teleport you to. Snakes send you back, while ladders send you forward.

A Cell object keeps track of any players that land on it. It is also aware of its location.

class Cell
  attr_reader :location, :players

  def initialize(input)
    @location = input.fetch(:location)
    @players = []
  end

  def exit(player)
    evict player
  end

  def enter(player, board)
    admit player
    player.set_cell(self)
  end

private

  def admit(player)
    players << player
  end

  def evict(player)
    players.delete(player)
  end
end

A Portal object inherits from Cell. It adds behaviour, and has a destination attribute. It overrides the enter method in its superclass. Instead of admitting the player it tells the board to send it to the portal's destination.

class Portal < Cell
  attr_reader :destination

  def initialize(input)
    @destination = input.fetch(:destination)
    super

    raise ArgumentError, "Location and destination can not be the same" if location.equal?(destination)
  end

  def enter(player, board)
    board.move player, location, destination
  end
end

The Player object must know of the Cell it resides in. It is also responsible for rolling the die.

class Player
  attr_reader :name
  attr_accessor :current_cell

  def initialize(input)
    @name = input.fetch(:name)
  end

  def to_s
    name
  end

  def roll_dice
    rand(1..6)
  end

  def set_cell(cell)
    self.current_cell = cell
  end

  def position
    if current_cell
      current_cell.location
    else
      -1 # represents off the board state
    end
  end
end

Finally, the Board keeps track of the current player, the number of turns, and moves players in between cells. It also decides if the game has a winner.

class Board
  attr_reader :grid, :players, :die
  attr_accessor :turn

  def initialize(input = {})
    @grid = input.fetch(:grid, default_grid)
    @players = []
    @turn = 0
  end

  def add_player(player)
    players << player
  end

  def get_cell(index)
    grid[index]
  end

  def move(player, from, to)
    get_cell(from).exit player if game_started?
    get_cell(to).enter player, self
  end

  def play_turn
    current_player_position = current_player.position
    roll = current_player.roll_dice

    if winner?(roll)
      puts "#{current_player} rolls a #{roll} and wins! Congratulations!"
      exit
    end

    move current_player, current_player.position, current_player.position + roll
    puts "#{current_player} rolls a #{roll} and moves from #{current_player.position - roll} to #{current_player.position}"

    increment_turn
    play_turn
  end

private

  def default_grid
    Array.new(100)
  end

  def current_player
    players.fetch(turn % players.size)
  end

  def increment_turn
    self.turn += 1
  end

  def game_started?
    turn > 0
  end

  def winner?(last_roll)
    # +1 is to account for arrays starting at 0. So cell 1 is in fact cell 2 on a board.
    (current_player.position + 1 + last_roll) >= grid.size
  end
end

Concerns

I think I've done a decent job for my first crack, but I know this can be improved.

  1. I am not sure about the die rolling and player moving mechanic. It feels clunky. Is passing the cell to the user a necessary dependency? I can't think of a better way to get the player to move and change its position.
  2. The play turn uses recursion, but again, I'm not sure how to write it better.

That arrays in Ruby start at 0 is causing me all sorts of confusion.

  1. To render the board I must add 1 to each cell location.
  2. I must add 1 to the player position or deduct 1 from the grid size to work out the winner.
  3. Players start off the board. I must check if the player has a cell, and if not, I have to return -1 to represent the off the board state.

There are probably lots of issues I'm not aware of. My objectives are the following. I think so far I might have succeeded with 2, but failed with 1 and 3.

  1. Make the code easy to modify. Let's say I want to add a Rule object that can store different winning criteria (roll to an excess of 100 or land exactly on 100).
  2. Introduce other cell subclasses with different effects.
  3. Make the game state easy to save so that I can player across HTTP requests.

Here's an example of running the game:

game  = SnakesAndLadders.classic
m = SnakesAndLadders::Player.new name: "Mario", color: "Red"
l = SnakesAndLadders::Player.new name: "Luigi", color: "Green"
game.add_player m
game.add_player l
game.play_turn