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)  endprivate  def admit(player)    players << player  end  def evict(player)    players.delete(player)  endendA 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  endendThe 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  endendFinally, 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  endprivate  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  endendConcernsI think I've done a decent job for my first crack, but I know this can be improved.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.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.To render the board I must add 1 to each cell location.I must add 1 to the player position or deduct 1 from the grid size to work out the winner.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.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).Introduce other cell subclasses with different effects.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.classicm = SnakesAndLadders::Player.new name: Mario, color: Redl = SnakesAndLadders::Player.new name: Luigi, color: Greengame.add_player mgame.add_player lgame.play_turn
Ruby object oriented Snakes and Ladders implementation
object oriented;ruby;game
some great work here! I will give you partial implementation that addresses some issues and (hopefully) will give you some idea for further refactoring.For start a simple wrapper for index/position. It wraps only integer because you use single-dimension world, but you could support multiple dimensions aswell (see comment in code for more detail).class Location  attr_reader :x  def initialize(x)    @x = x  end  # those methods has been overwritten because i want to use different  # instances of Location as hash keys. As long as @x is the same between  # two instances of Location it's considered as the same object, i.e.  #   # hash = {}  # hash[Location.new(1)] = :value  # hash[Location.new(1)] # => :value  # if you would want to add pair of coordinates to location you should  # update those methods to behave the same, i.e.  #  # def ==(other); x == other.x && y == other.y;end  # def hash; [x,y].hash; end  #  # hash[Location.new(1,0)] = :value  # hash[Location.new(1,0)] # => :value  #  # This is required only because I use hash as storage  def ==(other)    x == other.x  end  def eql?(other)    self.==(other)  end  def hash    x.hash  endendBoard looks like hash with some restrictions (size) and has default value of empty cell (Cell). Stores in memory only non-default values. A bit smarter than array in my opinion.Relying on hash means that the keys can't really be removed and memory won't be freed but it won't be issue if you decide to replace hash with something else as long as it implements contract (methods [] and []=). It can be replaced with some persistent storage (file/db), give it a try! Once you have in-memory and persistent storage you can use hash in tests (it works fast) and file/db board in your web/console application. This is great pattern but very difficult for many people who rely heavily on ActiveRecord. Don't let storage drive your design!class Board  def initialize(size=100)    @size = 100    @grid = Hash.new(Cell.new)  end  # we've added some extra methods to Location, here it pays off.  def [](location)    @grid[location]  end  def []=(location, value)    return nil if location.x < 0 || location.x > @size    @grid[location] = value  endendThe cell on board can be entered, that's it. It shares contract (interface) with Snake and Ladder.class Cell  def enter(player)  endendLadder is a bit different, on enter it teleports player. It shares contract with Cell. If you use rspec you should write shared example group for any kind of cell to ensure that it supports enter method.class Ladder  def initialize(destination)    @destination = destination  end  def enter(player)    player.location = @destination  endendFor simplicity just copy of Ladder, you could implement your own version to ensure that destination is lower than current location or some other behaviour.The Portal class didn't really fit into problem domain. Writing code is cheap, reading it later and understanding is not. If something can be done in more expressive way go for it.Snake = LadderPlayer knows how to move, to get make it work he needs to be aware of the board so we have to inject it. This knowledge could be extracted to some middle object but whatever it's worth it is up to you. Do not try to add too many classes if you don't need it, every class/file you add makes the application more difficult to follow/understand.I've made it this way because it's simple and demonstrates dependency inversion principle. Board (high level abstration) is being used by Player (lower level abstraction).There is little risk (or reason) of Board changing it's behaviour, on the other hand Player.move can be adjusted to incorporate some rules or restrictions (i.e. roll 6 three times in a row should invalidate the move), so risk of breaking the application is lower this way.class Player  attr_accessor :location, :name  def initialize(board, name = '')    @name = name    @board = board    @location = Location.new(0)  end  def move(num)    puts moving #{name} from #{@location.x} to #{@location.x + num}    @location = Location.new(@location.x + num)    @board[@location].enter(self)    @location.x  endendThe Dice.class Dice  def self.roll    1 + rand(6)  endendAnd finally sample usage (proof of work):@board = Board.new@board[Location.new(2)] = Ladder.new(Location.new(5))@board[Location.new(4)] = Snake.new(Location.new(3))@player1 = Player.new(@board, 'one')@player2 = Player.new(@board, 'two')@player1.move(1) #goes to position 1@player2.move(2) #enters position 2, there is ladder that teleports him to position 5loop do  @player1.move(Dice.roll)  @player2.move(Dice.roll)  break if @player1.location.x >= 100 || @player2.location.x >= 100endI strongly recommend you to look for presentations of Sandi Metz and get her book (Practical Object-Oriented Design in Ruby (POODR)) - you won't regret it.Hope you find this useful!