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