Example #1
0
class Turn:
    def __init__(self,state,coeffs,tc):
        self.state  = state
        self.coeffs = coeffs
        self.tc     = tc
        #self.update_bank_offer()
        # TODO: I do not know why this does not work.
        #if self.state.year == -1:
           #self.state.year = self.coeffs.starting_year

    def calc_maximum_effort(self):
        return sum([p.maximum_effort() for p in self.state.people]) - self.calc_school_effort()

    def reset(self):
        self.state.expenditure         = 0
        self.state.tons_to_market      = 0
        self.state.income              = 0
        self.state.initial_population  = self.state.population
        self.state.total_effort        = sum(self.state.efforts)
        self.state.fertilizer          = False
        self.state.high_yield_seeds    = False
        self.state.user_messages       = []
        self.state.died                = []
        self.state.died_reasons        = []

    def message(self,m):
        self.state.user_messages.append(m)

    def is_game_over(self):
        return self.is_everyone_dead() or self.is_debt_too_high()

    def is_everyone_dead(self):
        if self.state.population < 1:
            return True
        return False

    def is_debt_too_high(self):
        if self.state.cash < -100:
            return True
        return False

    def go(self):

        """ run one turn of the simulation. """

        self.state.people = list(setup_people(self.state,self.coeffs,self.tc))
        self.village = Village(self.state,self.coeffs,self.tc)

        # only run if they're already alive
        if self.is_game_over():
            return (False,self.state)

        self.reset()

        # allocate calories
        if not self.state.subsistence_met:
            self.buy_food()
            

        # buy and sell goods in the village market
        self.sell_items()
        self.purchase_items()
        
        self.doctor()
        self.pregnancy()
        self.education()

        self.state.maximum_effort = self.calc_maximum_effort()

        self.check_sick()

        # village-wide updates
        self.village.buy_improvements()
        self.free_bednets()

        self.village.update_precipitation()
        self.check_drought()

        self.village.update_population()
        self.village.check_epidemic()

        self.update_population_count()
        self.update_yield()
        self.update_subsistence()

        self.check_final_health()

        # And now we'll figure out whether subsistence was met for the *next* season
        # We have to do this after check_final_health in case someone died there
        if self.is_subsistence_met():
            self.state.subsistence_met = True
        else:
            self.state.subsistence_met = False

            if not self.cooker.was_sufficient:
                # TODO finer-grained messages about fuel usage
                # (e.g. if not enough propane, but was augmented with collected wood to meet req.)
                if self.cooker.raw_leftovers == 0:
                    self.message("insufficient food")  # TODO there's no user-facing message for this one
                elif 'stove' in self.state.owned_items:
                    self.message("insufficient propane")
                else:
                    self.message("insufficient wood")

        self.state.maximum_effort = self.calc_maximum_effort()
        self.update_finances()

        self.age_people()
        self.increment_time()
        self.age_bednets()
        self.update_population_count()

        # update state with changes to people
        self.state = marshall_people(self.state.people,self.state)

        self.state.total_effort = sum(self.state.efforts)
        self.update_points()
        self.state.try_for_child = False
        self.village.update_subsidy_offers()
        self.state.food_to_buy = 0
        if self.is_game_over():
            return (False,self.state)
        return (True,self.state)


    def free_bednets(self):
        if not self.coeffs.enable_free_bednets or \
           self.state.year != self.coeffs.starting_year + self.coeffs.free_bednet_year:
            # this only happens in a specific year
            return

        num_bednets = len([elem for elem in self.state.owned_items if elem == "bednet"])
        needed_bednets = self.state.population / 2
        
        if num_bednets < needed_bednets:
            #self.message("An NGO has distributed free bednets to the village and your family.")
            self.message('NGO bednets')
            self.state.owned_items.extend(['bednet'] * needed_bednets)
            self.state.bednet_ages.extend([0] * needed_bednets)


    def sell_items(self):
        # front end gives us a list of "|" seperated item,quantity pairs
        # NOTE: no validation to make sure they have the items to sell
        try:
            # just take this opportunity to keep the list clean
            self.state.owned_items.remove('')
        except:
            pass

        real_sold_items = []
        for i in self.state.sell_items:
            if i == "":
                continue
            item, quantity = i.split("|") 
            quantity = int(quantity)

            num_sold = 0
            for x in range(quantity):
                try:
                    self.state.owned_items.remove(item)
                    num_sold += 1
                except ValueError:
                    # trying to sell something we don't have
                    # or more than we have of that item
                    # eg, if they try to sell bednets on the
                    # turn that they've deteriorated and are now
                    # no longer in the owned_items
                    continue

            real_sold_items.append("%s|%s" % (item, num_sold))
            price = self.check_sell_price(item) * num_sold

            self.state.cash += price
            if item == "stove":
                self.state.stove = False
            if item == "improvedstove":
                self.state.improved_stove = False
            if item == "boat":
                self.state.boat = False
            if item == "dragnet":
                self.state.dragnet = False
        self.state.sell_items = real_sold_items

    def purchase_items(self):

        # pay for education first
        self.state.cash -= self.calc_school_cost()

        # front end gives us a list of "|" separated item,quantity pairs
        # NOTE: no validation to check if they have the money
        for i in self.state.purchase_items:
            if i == "":
                continue
            item,quantity = i.split("|")
            price = self.check_purchase_price(item) * int(quantity)
            self.state.cash -= price
            for x in range(int(quantity)):
                # some items only affect state, are not stored
                if item == "fertilizer":
                    self.state.fertilizer = True
                elif item == "propane":
                    self.state.amount_propane += 10 # 10kg propane per tank
                elif item == "high_yield_seeds":
                    self.state.high_yield_seeds = True
                else:
                    self.state.owned_items.append(item)
            if item == "bednet":
                self.state.bednet_ages.extend([0] * int(quantity))
            if item == "stove":
                self.state.stove = True
            if item == "improvedstove" or item == "improved_stove":
                self.state.improved_stove = True
            if item == "boat":
                self.state.boat = True
            if item == "dragnet":
                self.state.dragnet = True

    def food_cost(self):
        cost = self.coeffs.food_cost
        if self.state.meals:
            # school meals raise maize prices by 20%
            # TODO: should probably be a coefficient
            cost *= 1.2
        self.state.calculated_food_cost = cost
        return cost
        
    def subsistence_cost(self):
        return self.calc_family_needs() / 100 * self.food_cost()

    def buy_food(self):
        cost = self.state.food_to_buy * self.food_cost()
        self.state.cash -= cost
        self.state.amount_calories = self.state.amount_calories + (self.state.food_to_buy * 10 * 180.0)

    def check_purchase_price(self,item):
        prices = dict()
        for i,price in zip(self.coeffs.market_items,self.coeffs.market_purchase_prices):
            prices[i] = price
        multiplier = 1.0
        if self.state.road:
            multiplier = 0.8
        return multiplier * prices[item]
        
    def check_sell_price(self,item):
        prices = dict()
        for i,price in zip(self.coeffs.market_items,self.coeffs.market_sell_prices):
            prices[i] = price
        return prices[item]

    def update_points(self):
        """ calculate achievement points for the turn """

        X = 30.0
        F = 2.0

        # we don't actually keep track of what the initial village
        # population was after the first turn, so i'm hard-coding it at 1000
        # for now
        initial_num_families = 1000.0 / self.coeffs.avg_family_size
        
        D = (100.0 * X) / initial_num_families

        V = 1000000.0 / (self.village.raw_improvement_price('road') * 0.05)

        self.state.u_points += (
            (X * self.calc_family_total_health()) +
            (self.state.income) +
            (F * self.calc_new_improvements_value()) + 
            (V * self.village.calc_new_improvements_value()) + 
            (D * (self.state.village_births - self.state.village_deaths))
            )
        
        # don't let points go below 0
        self.state.u_points = int(max(self.state.u_points,0))

    def calc_family_total_health(self):
        if len(self.state.people) == 0:
            return 0
        return float(sum([p.health for p in self.state.people]))

    def calc_family_avg_health(self):
        if len(self.state.people) == 0:
            return 0
        return float(sum([p.health for p in self.state.people])) / float(len(self.state.people))

    def calc_school_effort(self):
        """ number of hours family members spent in school """
        primary   = len([p for p in self.state.people if p.in_primary_school()])
        secondary = len([p for p in self.state.people if p.in_secondary_school()])

        return (int(self.coeffs.primary_school_effort) * primary) + \
               (int(self.coeffs.secondary_school_effort) * secondary)

    def calc_school_cost(self):
        """ francs spent on education for children """
        n = len([p for p in self.state.people if p.in_secondary_school()])
        return n * self.coeffs.secondary_school_cost

    def family_improvements_count(self):
        """ count of how many family improvements have been made """
        improvements = ["bednet","improved_stove","stove","boat","dragnet"]
        return len([i for i in self.state.owned_items if i in improvements])

    def calc_new_improvements_value(self):
        value = 0
        for i in self.state.purchase_items:
            if i == "":
                continue
            item,quantity = i.split("|")
            price = self.check_purchase_price(item) * int(quantity)
            value += price
        return value

    def village_improvements_count(self):
        """ count of how many village improvements have been made """
        return self.village.improvements_count()

    def update_population_count(self):
        """ Ensure that the population count is correct """
        self.state.population = len(self.state.people)
        
    def doctor(self):
        for (p,doctor) in zip(self.state.people,self.state.doctor):
            if doctor:
                p.visit_doctor()
                cost = self.coeffs.doctor_visit_cost
                if self.state.clinic:
                    # doctor visits are 20% normal when there's a clinic
                    cost *= .2
                self.state.cash -= cost

    def get_mother(self):
        """ retrieve the "mother" """
        for p in self.state.people:
            if p.name == 'Fatou':
                return p
            
        # if we make it here, Fatou must be dead
        # return a NullPerson instead
        class NullPerson:
            pregnant = False
            def make_pregnant(self):
                pass
        return NullPerson()


    def pregnancy(self):
        if self.state.try_for_child:
            # make Fatou pregnant
            fatou = self.get_mother()
            fatou.make_pregnant()
            #if fatou.pregnant:
                #self.message("%s is now pregnant." % fatou.name)
                #self.message(('pregnant',fatou.name))


    def education(self):
        for (p,enroll) in zip(self.state.people,self.state.enroll):
            p.update_schooling_state(enroll=enroll) 
            p.update_education()

    def children(self):
        """ have children, if any """
        if self.state.try_for_child:
            return False
        fatou = self.get_mother()
        baby = None
        if fatou.pregnant:
            baby = new_child(self.tc,self.coeffs,self.state)
            self.message('child born')
            self.state.people.append(baby)
            fatou.pregnant = False
            
            # maternal mortality check
            maternal_mortality_probability = 5
            if self.state.clinic:
                maternal_mortality_probability = 1
            # do it as greater than 100 - probability instead of
            # < probability because of the way rand_n() is stubbed out
            # in the unit tests to always return the lowest value
            if self.rand_n(100) > (100 - maternal_mortality_probability):
                self.state.died.append(fatou.name)
                self.state.died_reasons.append("childbirth")
                self.state.deaths += 1
                self.state.people.remove(fatou)
        self.update_population_count()
        if baby is not None:
            return True
        return False

    def update_finances(self):
        """ calculate the family's finances """
        self.determine_income()
        family_income_and_interest = self.income_plus_interest(self.state.income)

        taxes = self.village.family_taxes(family_income_and_interest)

        self.state.cash = self.determine_cash(self.state.income, self.state.cash, taxes)

        self.village.collect_taxes(taxes)

    def income_plus_interest(self,income):
        return max(float((float(income) * float(1.0 + self.coeffs.savings_rate))),0.0)

    def determine_cash(self,income,cash,taxes):
        if income < 0:
            return cash + income
        else:
            return cash + self.income_plus_interest(income) - taxes

    def increment_time(self):
        if self.state.season:
            self.state.year += 1
            self.state.season = False
        else:
            self.state.season = True

    def age_people(self):
        # TODO: subtly broken for children born in the wrong season
        if self.state.season:
            for p in self.state.people:
                p.age += 1

    def age_bednets(self):
        if 'bednet' not in self.state.owned_items:
            self.state.bednet_ages = []
            return
        try:
            self.state.bednet_ages.remove('')
        except:
            pass
        # age them
        self.state.bednet_ages = [age + 1 for age in self.state.bednet_ages]
        # if any are more than 5 years (10 turns)
        # we eliminate them
        to_remove = []
        for age in self.state.bednet_ages:
            if age > 9:
                try:
                    self.state.owned_items.remove('bednet')
                except ValueError:
                    # something weird.
                    pass
                to_remove.append(age)
                self.message('bednet deteriorated')
        for age in to_remove:
            try:
                self.state.bednet_ages.remove(age)
            except ValueError:
                pass

    def check_drought(self):
        self.state.drought = False

        # if there's irrigation, we have no drought
        if self.state.irrigation:
            return
        
        # anything less than the threshold is considered a drought
        if self.state.precipitation < self.coeffs.drought_threshold:
            self.state.drought = True

    def rand_n(self,n):
        return self.tc.randint(a=0,b=n,n=1).values[0]

    def check_final_health(self):
        dead = []
        for person in self.state.people:
            if person.is_dead():
                dead.append(person)
                self.state.died.append(person.name)
                self.state.died_reasons.append('unknown')   # TODO reason for death
        for d in dead:
            self.state.people.remove(d)
        self.state.deaths += len(dead)
        self.update_population_count()

    def check_sick(self):
        for p in self.state.people:
            p.check_sick(self.state)
            
    def update_yield(self):
        self.update_avg_productivity()
        self.update_amount_fish()
        self.update_soil_health()
        self.update_amount_maize()
        self.update_amount_cotton()
        self.update_amount_wood()
        self.update_amount_water()
        self.update_small_business_income()
        self.update_microfinance_bank()
        self.convert_calories()
        self.village.update_fish_stock()
        self.village.update_wood_stock()

    def t_boat(self):
        if self.state.boat:
            return self.coeffs.boat_coeff
        return 1.0

    def t_dragnet(self):
        if self.state.dragnet:
            return self.coeffs.dragnet_coeff
        return 1.0

    def t_fert(self):
        if self.state.fertilizer:
            return self.coeffs.fertilizer_coeff
        return 1.0

    def t_fert_cotton(self):
        if self.state.fertilizer:
            return self.coeffs.fertilizer_cotton_coeff
        return 1.0

    def t_irr(self):
        if self.state.irrigation:
            return self.coeffs.irrigation_coeff
        return 1.0

    def t_drought(self):
        # XXX TODO: fix tight coupling with check_drought()
        if self.state.drought:
            return self.coeffs.drought_coeff
        return 1.0

    def update_soil_health(self):
        """ updates the soil health depending on whether fertilizer has been used in the last three years """
        if self.state.fertilizer or self.state.fertilizer_t1 or \
               self.state.fertilizer_t2:
            self.state.soil_health = 1.0
        else:
            #self.message("Soil health is depleted.")
            self.state.soil_health = self.coeffs.soil_depletion
            
        if self.state.season:
            # once a year, rotate fertilizer history
            self.state.fertilizer_t2 = self.state.fertilizer_t1
            self.state.fertilizer_t1 = self.state.fertilizer or self.state.fertilizer_last_turn
        else:
            # it gets a little weird since we only care about the fertilizer
            # history per year, but it's a user option on each turn
            # so we use fertilizer_last_turn to fill in the gap
            self.state.fertilizer_last_turn = self.state.fertilizer

    def calc_amount_fish(self):
        assert self.state.effort_fishing >= 0
        assert self.coeffs.avg_fishing_yield >= 0
        assert self.state.avg_productivity >= 0
        assert self.t_boat() >= 0
        assert self.t_dragnet() >= 0
        assert self.state.fish_coeff >= 0
        return self.coeffs.fishing_effort_coeff * self.state.effort_fishing \
            * self.coeffs.avg_fishing_yield \
            * ((95.0 + self.rand_n(10)) / 100.0) \
            * self.state.avg_productivity * self.t_boat() \
            * self.t_dragnet() \
            * self.state.fish_coeff

    def update_amount_fish(self):
        self.update_fish_coeff()
        self.state.amount_fish = self.calc_amount_fish()
        if self.state.fishing_limit > 0 :
            self.state.amount_fish = min(self.state.fishing_limit,
                                         self.state.amount_fish)

    def calc_wood_coeff(self):
        if self.state.wood_stock >= self.coeffs.wood_k / 2.0:
            return 1
        else:
            assert self.coeffs.wood_k != 0
            return self.state.wood_stock / self.coeffs.wood_k

    def update_wood_coeff(self):
        # TODO: does wood_coeff actually need to be a state variable?
        self.state.wood_coeff = self.calc_wood_coeff()

    def calc_fish_coeff(self):
        if self.state.fish_stock < 0:
            self.state.fish_stock = 0
        if self.state.fish_stock >= self.coeffs.fish_k / 2.0:
            return 1.0
        else:
            assert self.coeffs.fish_k != 0
            return self.state.fish_stock / self.coeffs.fish_k

    def update_fish_coeff(self):
        self.state.fish_coeff = self.calc_fish_coeff()

    def percent_maize(self):
        """ what percent of the family's fields are growing Maize """
        if len(self.state.crops) == 0:
            return 0
        n_maize = len([x for x in self.state.crops if x.lower() == 'maize'])
        return float(n_maize) / len(self.state.crops)

    def percent_cotton(self):
        """ what percent of the family's fields are growing Cotton """
        if len(self.state.crops) == 0:
            return 0
        n_cotton = len([x for x in self.state.crops if x.lower() == 'cotton'])
        return float(n_cotton) / len(self.state.crops)

    def t_high_yield_seeds(self):
        if self.state.high_yield_seeds:
            return self.coeffs.maize_high_yield_seeds_multiplier
        return 1.0

    def calc_amount_maize(self):
        assert self.coeffs.productivity_effort_coeff >= 0
        assert self.state.effort_farming >= 0
        assert self.coeffs.avg_maize_yield >= 0
        assert self.state.avg_productivity >= 0
        assert self.state.soil_health >= 0
        assert self.t_fert() >= 0
        assert self.t_irr() >= 0
        assert self.t_high_yield_seeds() >= 0
        assert self.percent_maize() >= 0 and self.percent_maize() <= 1.0
        return max((
                ((self.coeffs.productivity_effort_coeff * self.state.effort_farming) ** self.coeffs.maize_productivity_exponent)
                * self.coeffs.avg_maize_yield
                * ((95.0 + self.rand_n(10)) / 100.0)
                * self.state.avg_productivity
                * self.t_drought() * self.t_irr()
                * self.state.soil_health * self.t_fert()
                * self.t_high_yield_seeds()
                * self.percent_maize())
                   , 0.0)
    
    def calc_amount_cotton(self):
        return max(((self.coeffs.productivity_effort_coeff * self.state.effort_farming) ** self.coeffs.cotton_productivity_exponent) \
                       * self.coeffs.avg_cotton_yield \
                       * ((95.0 + self.rand_n(10)) / 100.0) \
                       * self.state.avg_productivity \
                       * self.state.soil_health * self.t_fert_cotton() \
                       * self.t_drought() * self.t_irr() \
                       * self.percent_cotton()
                   , 0.0)

    def update_amount_cotton(self):
        self.state.amount_cotton = self.calc_amount_cotton()
        assert self.state.amount_cotton >= 0
        # update user messages
        if self.state.amount_cotton > 1:
            self.message('good cotton')
    
    def update_amount_maize(self):
        self.state.amount_maize = self.calc_amount_maize()
        assert self.state.amount_maize >= 0
        # update user messages
        if self.state.amount_maize > 0:
            if self.state.amount_maize < 1:
                self.message('poor maize')
        if self.state.amount_maize > 3:
            self.message('good maize')

    def calc_amount_wood(self):
        return self.coeffs.wood_effort_coeff * self.state.effort_fuel_wood \
            * self.coeffs.avg_wood_yield \
            * ((95.0 + self.rand_n(10)) / 100.0) \
            * self.state.avg_productivity * self.calc_wood_coeff()

    def update_amount_wood(self):
        self.state.amount_wood = self.calc_amount_wood()
        if self.state.wood_limit > 0 and \
                self.state.amount_wood > self.state.wood_limit:
            self.state.amount_wood = self.state.wood_limit
        assert self.state.amount_wood >= 0

    def calc_amount_water(self):
        return self.state.effort_water \
            * self.coeffs.avg_water_yield \
            * ((95.0 + self.rand_n(10)) / 100.0) \
            * self.calc_family_avg_health()

    def update_amount_water(self):
        self.state.amount_water = self.calc_amount_water()
        assert self.state.amount_water >= 0
        
    def calc_small_business_capital(self):
        return (1 - self.coeffs.small_business_depreciation_rate) * self.state.small_business_capital + self.state.small_business_investment

    def calc_small_business_income(self):
        # update capital, accounting for depreciation
        self.state.small_business_capital = self.calc_small_business_capital()
        self.state.cash -= self.state.small_business_investment
    
        assert self.state.effort_small_business >= 0
        assert self.state.small_business_capital >= 0
        #return self.state.effort_small_business \
        #       * self.coeffs.avg_small_business_yield \
        #       * ((95.0 + self.rand_n(10)) / 100.0) \
        #       * self.state.avg_productivity

        return self.coeffs.small_business_productivity_effect * \
            ( ((self.state.avg_productivity * self.state.effort_small_business) ** self.coeffs.small_business_diminishing_return) * \
                  (self.state.small_business_capital ** (1 - self.coeffs.small_business_diminishing_return)) \
                  ) * \
                  (1 + (self.coeffs.small_business_road_effect * self.state.road)) * \
                  (1 + (self.coeffs.small_business_electricity_effect * self.state.electricity)) * \
                  (self.coeffs.small_business_drought_effect * ((self.state.precipitation / self.coeffs.avg_precipitation) ** 0.4) ) * \
                  (self.coeffs.small_business_epidemic_effect * (1 - self.state.epidemic))

    def update_small_business_income(self):
        self.state.small_business_income = self.calc_small_business_income()
        assert self.state.small_business_income >= 0

    def calc_microfinance_max_borrow(self):
        return self.subsistence_cost() + self.state.small_business_capital
        
    def pay_microfinance_bank(self):
        # first from family fund, then business capital, then carryover
        amount_due = self.state.microfinance_amount_due
        amount_paid = 0.0
        if self.state.cash >= amount_due:
            self.state.cash -= amount_due
            amount_paid += amount_due
        elif self.state.cash < 0:
            amount_paid = 0.0
        else:
            amount_paid = self.state.cash
            self.state.cash = 0

        remaining_due = amount_due - amount_paid
        if self.state.small_business_capital >= remaining_due:
            self.state.small_business_capital -= remaining_due
            amount_paid += remaining_due
        else:
            amount_paid += self.state.small_business_capital
            self.state.small_business_capital = 0

        self.state.microfinance_balance -= amount_paid

        # save carryovers if we couldn't pay it all
        self.state.microfinance_amount_paid = amount_paid
        self.state.microfinance_amount_due -= amount_paid

        if self.state.microfinance_balance < 0:
            self.state.microfinance_balance = 0.0

        return

    def calc_microfinance_balance(self):
        interest = self.state.microfinance_balance * self.state.microfinance_interest_rate / 100

        self.state.microfinance_balance += interest
        payment_amount = self.state.microfinance_borrow / self.coeffs.microfinance_repay_period

        # add any amount carried over from the previous turn
        if self.state.microfinance_balance > payment_amount:
            self.state.microfinance_amount_due += payment_amount + interest
        else:
            self.state.microfinance_amount_due += self.state.microfinance_balance

        # now actually pay
        self.pay_microfinance_bank()
        return self.state.microfinance_balance

    def calc_microfinance_interest(self):
        return (self.coeffs.microfinance_base_interest * ((100.0 + self.rand_n(10)) / 100.0) \
                    + (self.coeffs.microfinance_drought_effect * (self.state.precipitation / self.coeffs.avg_precipitation)) \
                    + (self.coeffs.microfinance_epidemic_effect * self.state.epidemic))

    def update_microfinance_interest(self):
        self.state.microfinance_current_interest_rate = self.calc_microfinance_interest()

    def update_microfinance_bank(self):
        self.state.microfinance_amount_paid = 0
        if self.state.microfinance_balance == 0:
            self.state.microfinance_amount_due = 0
            if self.state.microfinance_borrow > 0:
                # new loan
                if self.state.microfinance_borrow > self.state.microfinance_max_borrow:
                    self.state.microfinance_borrow = self.state.microfinance_max_borrow
                self.state.microfinance_balance = self.state.microfinance_borrow
                self.state.cash += self.state.microfinance_borrow
            self.state.microfinance_interest_rate = \
                self.state.microfinance_current_interest_rate
        else:
            self.state.microfinance_balance = self.calc_microfinance_balance()

        # update loan offer
        self.update_bank_offer()
 

    def update_bank_offer(self):        # update loan offer
        self.state.microfinance_max_borrow = self.calc_microfinance_max_borrow()
        self.update_microfinance_interest()


    def convert_calories(self):
        assert self.state.amount_maize >= 0
        assert self.state.amount_fish >= 0
        assert self.coeffs.maize_cal_coeff >= 0
        assert self.coeffs.fish_cal_coeff >= 0
        self.state.maize_cals = self.state.amount_maize * self.coeffs.maize_cal_coeff
        self.state.fish_cals  = self.state.amount_fish  * self.coeffs.fish_cal_coeff
        assert self.state.maize_cals >= 0
        assert self.state.fish_cals >= 0

    @property
    def cooker(self):
        assert hasattr(self, '_cooker'), "You must first call new_cooker"
        return self._cooker

    def new_cooker(self):
        self._cooker = fuel.FuelSupply(self.coeffs, self.state)
        return self._cooker

    def calc_amount_calories(self):
        raw_food_cals = self.state.maize_cals + self.state.fish_cals
        cooker = self.new_cooker()
        cooked = cooker.convert(raw_food_cals)
        return cooked + self.school_meal_calories()

    def school_meal_calories(self):
        if not self.state.meals:
            return 0

        enrolled_children = len([p for p in self.state.people if
                                 p.schooling_state.startswith('enrolled')])
        return self.coeffs.school_meals_calories * enrolled_children


    def update_amount_calories(self):
        self.state.amount_calories = self.calc_amount_calories()

    def is_subsistence_met(self):
        return self.state.amount_calories >= self.calc_family_needs()

    def is_water_subsistence_met(self):
        return self.state.amount_water >= self.state.family_water_needs

    def calc_family_needs(self):
        return stateless_logic.family_food_requirements(
            self.state.population,
            self.coeffs.subsistence)

    def update_family_needs(self):
        self.state.family_needs = self.calc_family_needs()

    def calc_family_water_needs(self):
        return self.state.population * self.coeffs.w_subsistence

    def update_family_water_needs(self):
        self.state.family_water_needs = self.calc_family_water_needs()

    def update_subsistence(self):
        self.update_family_needs()
        self.update_family_water_needs()

        # first update family health based on stored food from last season
        if self.is_subsistence_met():
            for p in self.state.people:
                p.update_health(self.coeffs.subsistence,
                                self.state.clinic,
                                self.state.electricity)
                #p.increment_health(self.coeffs.health_increment)
        else:
            for p, daily_calories in zip(self.state.people, self.state.calorie_allocation):
                calories = daily_calories * 180
                p.update_health(calories, self.state.clinic, self.state.electricity)

        # now that we've figured out whether we had enough food stored 
        # for subsistence this *past* season, we need to have any new
        # children we're gonna have -- so that we can then figure out
        # whether we'll have enough food stored for subsistence *next*
        # season -- so that we can warn the player and give him a chance
        # to purchase extra needed food and/or allocate available calories
        new_children = self.children()
        if new_children:
            self.update_family_needs()
            self.update_family_water_needs()


        # now we'll update the stored calories based on our newly farmed fish & grain
        self.update_amount_calories()

        self.state.food_to_sell = self.cooker.raw_leftovers

        propane_remaining = self.cooker.stock('propane')
        self.state.report_propane_used = self.state.amount_propane - propane_remaining
        self.state.amount_propane = propane_remaining

        self.state.wood_income = self.coeffs.wood_price * \
            self.cooker.stock('wood') * fuel.Wood.coefficient(self.coeffs)

        if not self.state.water_pump:
            if not self.is_water_subsistence_met():
                self.message("insufficient water")
                self.dehydrate()

    def starve(self):
        # multiply by 180 in order to reverse the transformation to daily cals done in UI
        for (allocation,p) in zip(self.state.calorie_allocation,self.state.people):
            #p.starve(allocation * 180)
            p.update_health(allocation * 180, self.state.clinic, self.state.electricity)

    def dehydrate(self):
        for p in self.state.people:
            p.dehydrate(self.state.family_water_needs,
                        self.state.amount_water,
                        self.state.population)

    def num_infants(self):
        return len([p for p in self.state.people if p.is_infant()])

    def total_productivity(self):
        return sum([p.productivity() for p in self.state.people])

    def num_non_infants(self):
        return self.state.population - self.num_infants()

    def calc_avg_productivity(self):
        if self.num_non_infants() == 0:
            return 0.0
        if self.state.total_effort == 0:
            return 0.0
        s = 0.0
        for p in self.state.people:
            if p.is_infant():
                continue
            weighted = p.effort * p.productivity()
            s += weighted
        return (s / self.state.total_effort) / 100.0

    def update_avg_productivity(self):
        self.state.avg_productivity = self.calc_avg_productivity()

    def determine_income(self):
        self.state.income = 0
        self.state.tons_to_market = 0
        if self.state.food_to_sell > 0:
            (self.state.food_income,tons_food) = self.sell_food()
            self.state.income += self.state.food_income
        else:
            self.state.food_income = 0.0

        self.state.income += self.state.wood_income
        (cotton_income,tons_cotton) = self.sell_cotton()
        self.state.cotton_income = cotton_income
        self.state.income += cotton_income
        self.state.income += self.state.small_business_income
        self.state.tons_to_market = tons_cotton
        if (self.cost_of_wagon()) > 0 and (self.cost_of_wagon() > self.state.income + self.state.cash):
            # selling cotton would put them into debt because of
            # transport costs
            self.state.income -= cotton_income
            self.state.tons_to_market = 0
            self.state.cotton_income = 0
            self.message("transport too expensive")
        else:
            self.state.tons_to_market = tons_cotton
            self.state.expenditure = self.hire_wagon()
        self.state.income -= self.state.expenditure


    def transport_cost_per_ton(self):
        if self.state.road:
            return self.coeffs.transport_cost_road
        else:
            return self.coeffs.transport_cost_no_road

    def cost_of_wagon(self):
        return self.state.tons_to_market * self.transport_cost_per_ton() * 100.0

    def hire_wagon(self):
        cost_to_market = self.cost_of_wagon()
        self.state.cost_to_market = int(round(cost_to_market))
        return self.state.expenditure + cost_to_market

    def sell_food(self):
        totalToSell  = self.state.food_to_sell * (self.coeffs.maize_export_units / 100.0)
        income       = (totalToSell * self.coeffs.maize_price) / 1000.0
        if self.state.meals:
            # school meals raise prices by 20%
            income *= 1.20
        tons_to_market = totalToSell / 1000.0
        return (income,tons_to_market)

    def sell_cotton(self):
        total_to_sell = self.state.amount_cotton * (self.coeffs.cotton_export_units / 100.0)
        price = ((total_to_sell * self.coeffs.cotton_price) / 1000.0)
        tons_to_market = total_to_sell / 1000.0
        return (price,tons_to_market)