Only the attr_reader definition, without the attr_writer , will only prevent the @board variable from being @board . In other words, your Board class does not provide an interface for modifying what is stored in @board , but does nothing to prevent the initial value from being changed.
You can use freeze :
def create_board @board = Array.new(3) { Array.new(3).freeze } @board.freeze end
(also you do not need map )
Freezing a top-level array and nested ones will do what you describe, but I think it will also break your game, because modifications will be completely impossible.
I would suggest not exposing @board at all and consider it closed. Then you must set the interface to set the values ββon the board and provide a method for returning the view of the board displayed for reading.
class Board def initialize create_board end def []=(x, y, value) @board[x][y] = value end def board @board.map { |a| a.dup.freeze }.freeze end private def create_board @board = Array.new(3) { Array.new(3) } end end b = Board.new b.board # => [[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]] b[1,2] = "x" b[0,0] = "o" b.board # => [["o", nil, nil], [nil, nil, "x"], [nil, nil, nil]] b.board[0] = "junk" # RuntimeError: can't modify frozen Array b.board[0][1] = "junk" # RuntimeError: can't modify frozen Array b.board # => [["o", nil, nil], [nil, nil, "x"], [nil, nil, nil]]