nicknovitski/tabletop

View on GitHub
lib/tabletop/roll.rb

Summary

Maintainability
A
1 hr
Test Coverage
require_relative 'dice_pool'

module Tabletop
  
  class Possibility #:nodoc: all
    attr_reader :outcomes
    
    attr_reader :conditions

    def initialize(outcomes, conditions)
      @outcomes, @conditions = outcomes, conditions
    end
    
  end
    
  class Roll
    # Rolls of necessity have a Pool object against which they check possible results.
    attr_accessor :pool
    

    # The block contains methods that further detail the roll, as described below.  
    # pool must be (surprise!) a Pool.  It's optional, because if #sides is passed 
    # in the block, then #roll can be called with a number of dice to roll, and a 
    # new Pool object will be instantiated every time this is done. 
    def initialize(pool=nil, &block)
      if pool
        raise ArgumentError if !pool.respond_to?(:roll)
      end
      @pool = pool
      @possibilities = []
      @die_sides = nil
      @static_modifier = 0
      @roll_modifier = 0
      @result_set = false
      instance_eval(&block)
      unless @result_set
        set_result(:sum)
      end
    end
    
    
    # Returns either an array or nil.
    # + If a "difficulty" was set in the most recent call of #roll, and #result meets or exceeds it, then the first element will be "Success". 
    # + If the conditions of any of the roll's @possibilities are met (see #meets?), then their outcomes will be all following elements. 
    # + If none of these conditions are met, returns nil
    def effects
      results = []
      
      if @difficulty
        results << "Success" if result >= @difficulty
      end
      
      @possibilities.each do |poss|
        results.concat(check(poss))
      end
      
      results.compact!

      if results.empty?
        results = nil
      end
      results
    end
    
    # Without any options passed, calls Pool#roll on the roll's pool.  Returns the Roll.
    # 
    # opts can have a few different values:
    # :modifier:: adds to all subsequent calls to #sum, until #roll is called again
    # :pool:: if #sides was called in the initialize block, and this is set, then a Pool of appropriate sides and number is created and assigned to @pool.
    # :difficulty:: see #effects 
    #--
    # TODO: @difficulty changes back to nil if it is not called in a given roll.
    # TODO: abstract out ternary?
    def roll(opts={})
      @roll_modifier = opts[:modifier] ? opts[:modifier] : 0
      if @die_sides
        if opts[:pool] 
          @pool = opts[:pool].d(@die_sides)
        else
          raise ArgumentError
        end
      end
      if opts[:difficulty]
        @difficulty = opts[:difficulty]
      end
      @pool.roll
      self 
    end
    
    # Takes a Possibility, returns an Array containing nil if any of it's conditions
    # aren't met. Otherwise, returns an Array containing all the Possibility's
    # outcomes.  If any of those outcomes are Roll objects, they are rolled and their
    # #effects are returned as an outcome.
    #--
    # TODO: checks #result, not #sum
    def check(p) #:nodoc:
      conditions_met = true
      
      if p.conditions[:>=] and sum < p.conditions[:>=]
        conditions_met = false
      end
      
      if p.conditions[:==] and p.conditions[:==] != sum
        conditions_met = false
      end
      
      if conditions_met
        results = []
        p.outcomes.each do |outcome|
          if outcome.instance_of?(Roll)
            results << outcome.roll.effects
          else
            results << outcome
          end
        end
        results
      else
        [nil]
      end
    end
    
    # The sum of the values of dice in the pool, and any modifier set in 
    # instantiation (see #add), or the most recent call to #roll.
    def sum
      @pool.sum + @static_modifier + @roll_modifier
    end
    
    # Attaches an object to work with #meets?.
    # 
    # value:: An integer.
    # outcomes:: An array of values to contribute to #effects if #meets? is true.
    def at_least(value, *outcomes)
      @possibilities << Possibility.new(outcomes, :>= => value)
    end
    
    # Attaches an object to work with #meets?.
    #
    # values:: Can be either an integer, or a Range.  If it's a range, then #equals creates an object for each number in the range.  If it's an integer, it creates just one. 
    # outcomes:: An array of values to contribute to #effects if #meets? is true.
    #--
    # TODO: values can be an array
    def equals(values, *outcomes)
      if values.instance_of?(Range)
        values.each do |val|
          @possibilities << Possibility.new(outcomes, :== => val)
        end
      else
        @possibilities << Possibility.new(outcomes, :== => values)
      end
    end
    
    # Defines a #result method, used by #effects.
    # 
    # If symbol is ':count', then args must include a :at_least option, and #result will
    # be equal to the number of dice in @pool of value equal or greater than 
    # args[:at_least]. 
    # 
    # Optionally, args can also include a :doubles option, for values that add 2 to #result
    # 
    # In all other cases, #result is aliased to #sum
    # 
    # Meant to be used in the initialize block.
    #--
    # TODO: raise an error symbol is :count but :at_least is not set
    def set_result(symbol, args={})
      if symbol == :count
        @count_at_least = args[:at_least]
        @count_doubles = args[:doubles]
        def result
          normal = @pool.count {|die| die.value >= @count_at_least}
          extra = @count_doubles ? @pool.count {|die| die.value == @count_doubles} : 0
          normal + extra
        end
      else
        def result
          sum
        end
      end
      @result_set = true
    end
    
    # Sets a modifier that's added to #sum.  
    #
    # Meant to be used in the initialize block.
    def add(mod)
      @static_modifier = mod
    end
    
    # Sets a default die size.  If set, roll can be called with a :pool argument to 
    # create and roll a new Pool of the indicated number and type of dice.
    #
    # Can be an integer other than zero, or :fudge for fudgedice.
    #
    # Meant to be used in the initialize block.
    def sides(num_sides)
      @die_sides = num_sides
    end
    
  end
  
end