0
\$\begingroup\$

I'm trying to create a basic clone of Snakes and Ladders using object oriented design. I'm struggling to model my objects and their relationships. For the time being I'm interested in moving players around the board.

I came up with three main objects. A Board, a Cell, and a Player. There is also the concept of a grid, which is abstract. The idea that any grid representing the state of the game can be plugged into the board. The grid is an array where each element contains a Cell object.

  class Board
    attr_reader :grid

    def initialize(input = {})
      @grid = input.fetch(:grid, default_grid)
    end

    def get_cell(index)
      grid[index]
    end

    def add_player(index, player)
      get_cell(index).add_player(player)
    end

    def remove_player(index, player)
      get_cell(index).remove_player(player)
    end

    def move_player(from, to, player)
      remove_player(from, player)
      add_player(to, player)
    end

  private

    def default_grid
      Array.new(100)
    end
  end

A cell instance keeps an array of players who currently occupy the cell. I'm having difficulty because cells can have behaviour. They can be just a tile with no behaviour; a portal which sends the player somewhere (snake or a ladder); an exit, which has no behaviour but must be coupled with an entrance for rendering purposes (so the player would know what entrance the exit belongs to).

The only difference between a snake and a ladder is that a snake's destination is less than its location, while the opposite is true for a ladder. So here's my implementation of Cell

class Cell
  attr_reader :location, :destination, :origin, :players

  def initialize(input)
    @location = input.fetch(:location)
    @destination = input.fetch(:destination, nil)
    @origin = input.fetch(:origin, nil)

    @players = input.fetch(:players, [])
  end

  def add_player(player)
    players << player
  end

  def type
    if origin
      location < origin ? :snake_exit : :ladder_exit
    elsif destination
      destination < origin ? :snake : :ladder
    else
      :tile
    end
  end
end

My instinct tells me my Cell object is poorly designed. Especially the type method. But I don't know how to change my design. It's trying to be too many things.

As you can see below, the board can add and remove a player from cells:

    def add_player(index, player)
      get_cell(index).add_player(player)
    end

    def remove_player(index, player)
      get_cell(index).remove_player(player)
    end

    def move_player(from, to, player)
      remove_player(from, player)
      add_player(to, player)
    end

But what if a cell has behaviour? How does it tell the board to move the player? I thought of adding a method to the Board class:

def play_turn
  get_cell(player.position).remove_player(player)
  player.position += Dice.roll
  cell = get_cell(player.position)
  case cell.type
  when :snake, :ladder
    player.position = cell.location
    get_cell(player.position).add_player(player)
  else
    cell.add_player(player)
  end
  #...
end

But my instinct tells me this is wrong. What if I decide to modify the game and add more cell types other than snakes and ladders? I would have to do more type checks and decide what methods to execute.

if cell.type == :snake
  # cell.do_something
elsif cell.type == :black_hole
  cell.consume_player(player)

This raises the question of whose responsibility is it to move the player? Instinctively, since teleporting to another location is the cell's behaviour, it should be the cell, right?

But how would that work when the cell has no knowledge of other cells? A cell has no knowledge of other cells so it can't possibly add the player to another cell. The board does this now.

But if moving the player is not the cell's job, then the Cell has no behaviour. It's just a data container that the board will use to make a decision on what to do with the player.

Maybe it's the responsibility of the player object? But it also has no knowledge of what cells are.

Another question is who should track player's positions? The board? The player, or the cell itself?

Sorry, there are a lot of questions here. But this is a complex problem, and I'm looking for guidance on how to simulate a player's movement on the board. I feel like my objects are poorly designed.

For the sake of completeness here is my player's class:

class Player
  attr_reader :name, :color, :position

  def initialize(name, color, position = 0)
    @name, @color, @position = name, color, position
  end
end
\$\endgroup\$
1
\$\begingroup\$

I'm having difficulty because cells can have behaviour. They can be just a tile with no behaviour; a portal which sends the player somewhere (snake or a ladder); an exit, which has no behaviour but must be coupled with an entrance for rendering purposes (so the player would know what entrance the exit belongs to).


But my instinct tells me this is wrong. What if I decide to modify the game and add more cell types other than snakes and ladders? I would have to do more type checks and decide what methods to execute.

It sounds like you should utilize some form of inheritance to to give each type of cell unique behavior, and to allow you to easily add new kinds of cells. For example, you could modify your Cell class to not have a type, but instead have a method called onLanding(Board, Player). The basic Cell class would just add that player to itself using the methods provided by Board. Then you could create other types of cell classes that are derived from Cell and override its onLanding method to move the player to another tile, change the player's name or color, or any other functionality that your Board and Player classes provide in their public interfaces.

The Board class would not have to know what each type of cell in its grid is; in its play_turn method, it would just call the cell's onLanding method whenever a player is moved to that cell. Then the cell can do whatever it needs to do to the player.

Another question is who should track player's positions? The board? The player, or the cell itself?

Currently, that sounds like that would be most fitting to your Board class.

It might be a good idea to move Board's play_turn method to another class, though its not necessary. You could create a new class that handles the game play and game rules, while Board's primary responsibility would be to contain the current state of the game board, such as player positions and cells. The new class could then update an instance of Board to reflect changes in the game state.

I admit I'm not any kind of master of OOD, but I hope my suggestions at least give you some ideas.

\$\endgroup\$
  • \$\begingroup\$ Thanks. I've implemented something similar to your suggestion. I passed in the board to the cell so that the cell can ask the board to "teleport" the player. Dependency inject was too obvious, I guess. I still haven't decided if changing the player's position is the responsibility of the player or the board. Moving the player is up to the board now, but the player also has to keep it's position internally. For now I'm asking the cell to pass itself to the player. So asking the player for its position will ask the player's cell for its location. \$\endgroup\$ – Mohamad Nov 25 '14 at 11:58

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.