# frozen_string_literal: true module GameOfLife # Class representing a cell in the GemeOfLife universe # @!attribute [r] alive # @return [True | False] the living state of the cell # @!attribute [r] x # @return [Integer] the X-axis position of the cell in the universe # @!attribute [r] y # @return [Integer] the Y-axis position of the cell in the universe class Cell attr_reader :alive, :x, :y # Initialize a cell in the game of life universe # @param alive [True|False] boolean indicating if the cell is considered alive # @param x [Integer] the x position on the cyclic universe plane # @param y [Integer] the y position on the cyclic universe plane def initialize(alive:, x:, y:) @alive = alive @x = x @y = y end # Representation of a cell # Both LIVE_CELL, and DEAD_CELL constant are defined on the application start from the options # @return [String] if a cell is alive or an empty space def to_s alive? ? LIVE_CELL : DEAD_CELL end # Evolve a cell in the game of life to the new state in the next generation # @param neighbors [Array] that are neighboring the current cell (size of 8) # @return [GameOfLife::Cell] representing the new state in the next generation # # @note There are some optimizations of returning a new object only when the state changes, otherwise returning self # This optimization is done to limit the spacial complexity to `O(2n^2) = O(2*witdht*height)` # in the worst case scenario, and to `O(n^2) = O(width * hight)` in the best case scenario # (when the universe is stale and don't evolve anymore). # @see #living_neighbors def evolve!(neighbors) return repopulate! if dead? && living_neighbors(neighbors) == 3 return survive! if alive? && [2, 3].include?(living_neighbors(neighbors)) die! end # Helper method to define the language used in GameOfLife # @return [True | False] cell living state def alive? @alive end # Helper method to define the language used in GameOfLife # @return [True | False] cell dead state # @see #alive? def dead? !alive? end # Count the living neighbor cells relatively to the current cell in the {GameOfLife::Universe} # @param neighbors [Array] that are neighboring the current cell (size of 8) # @return [Integer] the number of living neighbors # @see GameOfLive::Universe#neighbors private def living_neighbors(neighbors) neighbors.select(&:alive?).size end # Helper method to define the language used in GameOfLife # @return [GameOfLife::Cell] new living cell # @see #survive! # @see #die! private def repopulate! self.class.new(x: x, y: y, alive: true) end # Helper method to define the language used in GameOfLife # @return [GameOfLife::Cell] self (since no change in state needed) # @see #repopulate! # @see #die! private def survive! self end # Helper method to define the language used in GameOfLife # When a cell is dead returns self otherwise return a new dead cell # @return [GameOfLife::Cell] self (if already dead) or new dead cell # @see #repopulate! # @see #survive! private def die! dead? ? self : self.class.new(x: x, y: y, alive: false) end end end