Exemplo n.º 1
0
	def _create_production_line(self, prod_line_id):
		"""Returns a non-changeable production line instance"""
		try:
			return ProductionLine.data[prod_line_id]
		except KeyError:
			ProductionLine.load_data(prod_line_id)
			return ProductionLine.data[prod_line_id]
Exemplo n.º 2
0
	def _create_production_line(self, prod_line_id):
		"""Returns a non-changeable production line instance"""
		try:
			return ProductionLine.data[prod_line_id]
		except KeyError:
			ProductionLine.load_data(prod_line_id)
			return ProductionLine.data[prod_line_id]
	def test_reset(self):
		self.add_line(1, 10, 0)
		self.assertFalse(ProductionLine.data)

		ProductionLine.load_data(1)
		self.assertTrue(ProductionLine.data)

		ProductionLine.reset()
		self.assertFalse(ProductionLine.data)
	def test_reset(self):
		self.add_line(1, 10, 0)
		self.assertFalse(ProductionLine._data)

		ProductionLine.get_const_production_line(1)
		self.assertTrue(ProductionLine._data)

		ProductionLine.reset()
		self.assertFalse(ProductionLine._data)
	def test_change_amount(self):
		self.add_line(1, 10, 0, {2: 3, 4: -5})
		line = ProductionLine(1)

		line.change_amount(2, 10)
		self.assertEqual(line.production, {2: 10, 4: -5})
		self.assertEqual(line.produced_res, {2: 10})
		self.assertEqual(line.consumed_res, {4: -5})

		line.change_amount(4, -1)
		self.assertEqual(line.production, {2: 10, 4: -1})
		self.assertEqual(line.produced_res, {2: 10})
		self.assertEqual(line.consumed_res, {4: -1})
Exemplo n.º 6
0
    def __init__(self,
                 inventory,
                 owner_inventory,
                 prod_id,
                 prod_data,
                 start_finished=False,
                 load=False,
                 **kwargs):
        """
		@param inventory: interface to the world, take res from here and put output back there
		@param owner_inventory: same as inventory, but for gold. Usually the players'.
		@param prod_id: int id of the production line
		@param prod_data: ?
		@param start_finished: Whether to start at the final state of a production
		@param load: set to true if this production is supposed to load a saved production
		"""
        super(Production, self).__init__(**kwargs)
        # this has grown to be a bit weird compared to other init/loads
        # __init__ is always called before load, therefore load just overwrites some of the values here
        self._state_history = deque()
        self.prod_id = prod_id
        self.prod_data = prod_data
        self.__start_finished = start_finished
        self.inventory = inventory
        self.owner_inventory = owner_inventory

        self._pause_remaining_ticks = None  # only used in pause()
        self._pause_old_state = None  # only used in pause()

        self._creation_tick = Scheduler().cur_tick

        assert isinstance(prod_id, int)
        self._prod_line = ProductionLine(id=prod_id, data=prod_data)

        if self.__class__.keep_original_prod_line:  # used by unit productions
            self.original_prod_line = self._prod_line.get_original_copy()

        if not load:
            # init production to start right away

            if self.__start_finished:
                # finish the production
                self._give_produced_res()

            self._state = PRODUCTION.STATES.waiting_for_res
            self._add_listeners(check_now=True)
Exemplo n.º 7
0
 def __init_production_lines(self):
     production_lines = self._get_producer_building(
     ).get_component_template(Producer)['productionlines']
     for key, value in production_lines.iteritems():
         production_line = ProductionLine(key, value)
         assert len(production_line.produced_res) == 1
         self.lines[production_line.produced_res.keys()
                    [0]] = production_line
	def test_change_amount(self):
		data = {
		        'time': 10,
		        'produces': [[2, 3]],
		        'consumes': [[4, -5]]
		}
		line = ProductionLine(1, data)

		line.change_amount(2, 10)
		self.assertEqual(line.production, {2: 10, 4: -5})
		self.assertEqual(line.produced_res, {2: 10})
		self.assertEqual(line.consumed_res, {4: -5})

		line.change_amount(4, -1)
		self.assertEqual(line.production, {2: 10, 4: -1})
		self.assertEqual(line.produced_res, {2: 10})
		self.assertEqual(line.consumed_res, {4: -1})
Exemplo n.º 9
0
	def __init_production_lines(self):
		production_lines = self._get_producer_building().get_component_template(Producer)['productionlines']
		for key, value in production_lines.iteritems():
			production_line = ProductionLine(key, value)
			production_line.id = None
			production_line.production = {}
			production_line.produced_res = {}
			for resource_id, amount in production_line.consumed_res.iteritems():
				production_line.production[resource_id] = -amount
				production_line.produced_res[resource_id] = -amount
			production_line.consumed_res = {}
			self.lines[production_line.produced_res.keys()[0]] = production_line
Exemplo n.º 10
0
    def get_unit_production_queue(self):
        """Returns a list unit type ids that are going to be produced.
		Does not include the currently produced unit. List is in order."""
        queue = []
        for prod_line_id in self.production_queue:
            prod_line = ProductionLine(prod_line_id,
                                       self.production_lines[prod_line_id])
            units = prod_line.unit_production.keys()
            if len(units) > 1:
                print 'WARNING: unit production system has been designed for 1 type per order'
            queue.append(units[0])
        return queue
Exemplo n.º 11
0
    def test_alter_production_time(self):
        data = {'time': 10}
        line = ProductionLine(1, data)

        self.assertEqual(line.time, 10)

        line.alter_production_time(2)
        self.assertEqual(line.time, 20)

        # Test that it modifies the original value (10)
        line.alter_production_time(2)
        self.assertEqual(line.time, 20)

        line.alter_production_time(1.5)
        self.assertEqual(line.time, 15.0)
	def __init_production_lines(self):
		production_lines = self._get_producer_building().get_component_template(Producer)['productionlines']
		for key, value in production_lines.iteritems():
			production_line = ProductionLine(key, value)
			production_line.id = None
			production_line.production = {}
			production_line.produced_res = {}
			for resource_id, amount in production_line.consumed_res.iteritems():
				production_line.production[resource_id] = -amount
				production_line.produced_res[resource_id] = -amount
			production_line.consumed_res = {}
			self.lines[production_line.produced_res.keys()[0]] = production_line
Exemplo n.º 13
0
	def __init(self, inventory, owner_inventory, prod_id, prod_data, state, creation_tick, pause_old_state = None):
		"""
		@param inventory: inventory of assigned building
		@param prod_line_id: id of production line.
		"""
		self.inventory = inventory
		self.owner_inventory = owner_inventory
		self._state = state
		self._pause_remaining_ticks = None # only used in pause()
		self._pause_old_state = pause_old_state # only used in pause()
		self._creation_tick = creation_tick

		assert isinstance(prod_id, int)
		self._prod_line = ProductionLine(id=prod_id, data=prod_data)
Exemplo n.º 14
0
	def test_alter_production_time(self):
		data = { 'time': 10 }
		line = ProductionLine(1, data)

		self.assertEqual(line.time, 10)

		line.alter_production_time(2)
		self.assertEqual(line.time, 20)

		# Test that it modifies the original value (10)
		line.alter_production_time(2)
		self.assertEqual(line.time, 20)

		line.alter_production_time(1.5)
		self.assertEqual(line.time, 15.0)
	def test_alter_production_time(self):
		self.add_line(1, 10, 0)
		line = ProductionLine(1)

		self.assertEqual(line.time, 10)

		line.alter_production_time(2)
		self.assertEqual(line.time, 20)

		# Test that it modifies the original value (10)
		line.alter_production_time(2)
		self.assertEqual(line.time, 20)

		line.alter_production_time(1.5)
		self.assertEqual(line.time, 15.0)
	def test_init(self):
		# NOTE: this has been broken by optimizations and will soon be moved to yaml, therefore not fixing it now
		#self.add_line(1, {10: 4, 12: 8})

		data = {'enabled_by_default': False,
		        'time': 90,
		        'level': [0, 1, 2],
		        'changes_animation': False,
		        'produces': [[14, 1]],
		        'consumes': [[19, -1]]
		}
		line = ProductionLine(1, data)
		self.assertEqual(line.time, 90)
		self.assertEqual(line.changes_animation, False)
		self.assertEqual(line.production, {14: 1, 19: -1})
		self.assertEqual(line.produced_res, {14: 1})
		self.assertEqual(line.consumed_res, {19: -1})
Exemplo n.º 17
0
	def __init__(self, building_id, name, settler_level, production_line_ids):
		super(AbstractFakeResourceDeposit, self).__init__(building_id, name, settler_level, [])
		self.lines = {} # output_resource_id: ProductionLine
		assert len(production_line_ids) == 1, 'expected exactly 1 production line'
		for production_line_id in production_line_ids:
			# create a fake production line that is similar to the higher level building one
			# TODO: use a better way of producing fake ProductionLine-s
			production_line = ProductionLine(production_line_id)
			production_line.id = None
			production_line.production = {}
			production_line.produced_res = {}
			for resource_id, amount in production_line.consumed_res.iteritems():
				production_line.production[resource_id] = -amount
				production_line.produced_res[resource_id] = -amount
			production_line.consumed_res = {}
			self.lines[production_line.produced_res.keys()[0]] = production_line
Exemplo n.º 18
0
    def test_change_amount(self):
        data = {'time': 10, 'produces': [[2, 3]], 'consumes': [[4, -5]]}
        line = ProductionLine(1, data)

        line.change_amount(2, 10)
        self.assertEqual(line.production, {2: 10, 4: -5})
        self.assertEqual(line.produced_res, {2: 10})
        self.assertEqual(line.consumed_res, {4: -5})

        line.change_amount(4, -1)
        self.assertEqual(line.production, {2: 10, 4: -1})
        self.assertEqual(line.produced_res, {2: 10})
        self.assertEqual(line.consumed_res, {4: -1})
Exemplo n.º 19
0
	def __init__(self, inventory, owner_inventory, prod_id, prod_data, \
	             start_finished=False, load=False, **kwargs):
		"""
		@param inventory: interface to the world, take res from here and put output back there
		@param owner_inventory: same as inventory, but for gold. Usually the players'.
		@param prod_id: int id of the production line
		@param prod_data: ?
		@param start_finished: Whether to start at the final state of a production
		@param load: set to true if this production is supposed to load a saved production
		"""
		super(Production, self).__init__(**kwargs)
		# this has grown to be a bit weird compared to other init/loads
		# __init__ is always called before load, therefore load just overwrites some of the values here
		self._state_history = deque()
		self.prod_id = prod_id
		self.prod_data = prod_data
		self.__start_finished = start_finished
		self.inventory = inventory
		self.owner_inventory = owner_inventory

		self._pause_remaining_ticks = None # only used in pause()
		self._pause_old_state = None # only used in pause()

		self._creation_tick = Scheduler().cur_tick

		assert isinstance(prod_id, int)
		self._prod_line = ProductionLine(id=prod_id, data=prod_data)

		if self.__class__.keep_original_prod_line: # used by unit productions
			self.original_prod_line = self._prod_line.get_original_copy()

		if not load:
			# init production to start right away

			if self.__start_finished:
				# finish the production
				self._give_produced_res()

			self._state = PRODUCTION.STATES.waiting_for_res
			self._add_listeners(check_now=True)
Exemplo n.º 20
0
class Production(ChangeListener):
    """Class for production to be used by ResourceHandler.
	Controls production and starts it by watching the assigned building's inventory,
	which is virtually the only "interface" to the building.
	This ensures independence and encapsulation from the building code.

	A Production is active by default, but you can pause it.

	Before we can start production, we check certain assertions, i.e. if we have all the
	resources, that the production takes and if there is enough space to store the produced goods.

	It has basic states, which are useful for e.g. setting animation. Changes in state
	can be observed via ChangeListener interface."""
    log = logging.getLogger('world.production')

    # optimisation:
    # the special resource gold is only stored in the player's inventory.
    # If productions want to use it, they will observer every change of it, which results in
    # a lot calls. Therefore, this is not done by default but only for few subclasses that actually need it.
    uses_gold = False

    keep_original_prod_line = False

    ## INIT/DESTRUCT
    def __init__(self,
                 inventory,
                 owner_inventory,
                 prod_id,
                 prod_data,
                 start_finished=False,
                 load=False,
                 **kwargs):
        """
		@param inventory: interface to the world, take res from here and put output back there
		@param owner_inventory: same as inventory, but for gold. Usually the players'.
		@param prod_id: int id of the production line
		@param prod_data: ?
		@param start_finished: Whether to start at the final state of a production
		@param load: set to true if this production is supposed to load a saved production
		"""
        super(Production, self).__init__(**kwargs)
        # this has grown to be a bit weird compared to other init/loads
        # __init__ is always called before load, therefore load just overwrites some of the values here
        self._state_history = deque()
        self.prod_id = prod_id
        self.prod_data = prod_data
        self.__start_finished = start_finished
        self.inventory = inventory
        self.owner_inventory = owner_inventory

        self._pause_remaining_ticks = None  # only used in pause()
        self._pause_old_state = None  # only used in pause()

        self._creation_tick = Scheduler().cur_tick

        assert isinstance(prod_id, int)
        self._prod_line = ProductionLine(id=prod_id, data=prod_data)

        if self.__class__.keep_original_prod_line:  # used by unit productions
            self.original_prod_line = self._prod_line.get_original_copy()

        if not load:
            # init production to start right away

            if self.__start_finished:
                # finish the production
                self._give_produced_res()

            self._state = PRODUCTION.STATES.waiting_for_res
            self._add_listeners(check_now=True)

    def save(self, db, owner_id):
        """owner_id: worldid of the owner of the producer object that owns this production"""
        self._clean_state_history()
        current_tick = Scheduler().cur_tick
        translated_creation_tick = self._creation_tick - current_tick + 1  #  pre-translate the tick number for the loading process

        remaining_ticks = None
        if self._state == PRODUCTION.STATES.paused:
            remaining_ticks = self._pause_remaining_ticks
        elif self._state == PRODUCTION.STATES.producing:
            remaining_ticks = Scheduler().get_remaining_ticks(
                self, self._get_producing_callback())
        # use a number > 0 for ticks
        if remaining_ticks < 1:
            remaining_ticks = 1
        db(
            'INSERT INTO production(rowid, state, prod_line_id, remaining_ticks, \
		      _pause_old_state, creation_tick, owner) VALUES(?, ?, ?, ?, ?, ?, ?)',
            None, self._state.index, self._prod_line.id, remaining_ticks, None
            if self._pause_old_state is None else self._pause_old_state.index,
            translated_creation_tick, owner_id)

        # save state history
        for tick, state in self._state_history:
            # pre-translate the tick number for the loading process
            translated_tick = tick - current_tick + 1
            db(
                "INSERT INTO production_state_history(production, tick, state, object_id) VALUES(?, ?, ?, ?)",
                self.prod_id, translated_tick, state, owner_id)

    def load(self, db, worldid):
        # NOTE: __init__ must have been called with load=True
        # worldid is the world id of the producer component instance calling this
        super(Production, self).load(db, worldid)

        db_data = db.get_production_by_id_and_owner(self.prod_id, worldid)
        self._creation_tick = db_data[5]
        self._state = PRODUCTION.STATES[db_data[0]]
        self._pause_old_state = None if db_data[
            4] is None else PRODUCTION.STATES[db_data[4]]
        if self._state == PRODUCTION.STATES.paused:
            self._pause_remaining_ticks = db_data[3]
        elif self._state == PRODUCTION.STATES.producing:
            Scheduler().add_new_object(self._get_producing_callback(), self,
                                       db_data[3])
        elif self._state == PRODUCTION.STATES.waiting_for_res or \
             self._state == PRODUCTION.STATES.inventory_full:
            # no need to call now, this just restores the state before
            # saving, where it hasn't triggered yet, therefore it won't now
            self._add_listeners()

        self._state_history = db.get_production_state_history(
            worldid, self.prod_id)

    def remove(self):
        self._remove_listeners()
        Scheduler().rem_all_classinst_calls(self)
        super(Production, self).remove()

    ## INTERFACE METHODS
    def get_production_line_id(self):
        """Returns id of production line"""
        return self._prod_line.id

    def get_consumed_resources(self):
        """Res that are consumed here. Returns dict {res:amount}. Interface for _prod_line."""
        return self._prod_line.consumed_res

    def get_produced_resources(self):
        """Res that are produced here. Returns dict {res:amount}. Interface for _prod_line."""
        return self._prod_line.produced_res

    def get_production_time(self):
        return self._prod_line.time

    def get_produced_units(self):
        """@return dict of produced units {unit_id: amount}"""
        return self._prod_line.unit_production

    def changes_animation(self):
        """Returns whether the production should change the animation"""
        return self._prod_line.changes_animation

    def get_state(self):
        """Returns the Production's current state"""
        return self._state

    def get_animating_state(self):
        """Returns the production's current state,
		but only if it effects the animation, else None"""
        if self._prod_line.changes_animation:
            return self._state
        else:
            return None

    def toggle_pause(self):
        if self.is_paused():
            self.pause()
        else:
            self.pause(pause=False)

    def is_paused(self):
        return self._state == PRODUCTION.STATES.paused

    def pause(self, pause=True):
        self.log.debug("Production pause: %s", pause)
        if not pause:  # do unpause
            # switch state
            self._state = self._pause_old_state
            self._pause_old_state = None

            # apply state
            if self._state in (PRODUCTION.STATES.waiting_for_res,
                               PRODUCTION.STATES.inventory_full,
                               PRODUCTION.STATES.done):
                # just restore watching
                self._add_listeners(check_now=True)

            elif self._state == PRODUCTION.STATES.producing:
                # restore scheduler call
                Scheduler().add_new_object(self._get_producing_callback(),
                                           self, self._pause_remaining_ticks)
            else:
                assert False, 'Unhandled production state: %s' % self._pause_old_state
        else:  # do pause
            # switch state
            self._pause_old_state = self._state
            self._state = PRODUCTION.STATES.paused

            if self._pause_old_state in (PRODUCTION.STATES.waiting_for_res,
                                         PRODUCTION.STATES.inventory_full,
                                         PRODUCTION.STATES.done):
                self._remove_listeners()
            elif self._pause_old_state == PRODUCTION.STATES.producing:
                # save when production finishes and remove that call
                self._pause_remaining_ticks = \
                  Scheduler().get_remaining_ticks(self, self._get_producing_callback())
                Scheduler().rem_call(self, self._get_producing_callback())
            else:
                assert False, 'Unhandled production state: %s' % self._pause_old_state

        self._changed()

    def finish_production_now(self):
        """Makes the production finish now"""
        if self._state != PRODUCTION.STATES.producing:
            return
        Scheduler().rem_call(self, self._get_producing_callback())
        self._finished_producing()

    def alter_production_time(self, modifier):
        """@see ProductionLine.alter_production_time"""
        try:
            self._prod_line.alter_production_time(modifier)
        except AttributeError:  # production line doesn't have this alter method
            pass

    def get_state_history_times(self, ignore_pause):
        """
		Returns the part of time 0 <= x <= 1 the production has been in a state during the last history_length ticks.
		"""
        self._clean_state_history()
        result = defaultdict(lambda: 0)
        current_tick = Scheduler().cur_tick
        pause_state = PRODUCTION.STATES.paused.index
        first_relevant_tick = self._get_first_relevant_tick(ignore_pause)
        num_entries = len(self._state_history)

        for i in xrange(num_entries):
            if ignore_pause and self._state_history[i][1] == pause_state:
                continue
            tick = self._state_history[i][0]
            if tick >= current_tick:
                break

            next_tick = min(
                self._state_history[i + 1][0],
                current_tick) if i + 1 < num_entries else current_tick
            if next_tick <= first_relevant_tick:
                continue
            relevant_ticks = next_tick - tick
            if tick < first_relevant_tick:
                # the beginning is not relevant
                relevant_ticks -= first_relevant_tick - tick
            result[self._state_history[i][1]] += relevant_ticks

        total_length = sum(result.itervalues())
        if total_length == 0:
            return result
        for key in result:
            result[key] /= float(total_length)
        return result

    def get_age(self):
        return Scheduler().cur_tick - self._creation_tick

    def get_unstorable_produced_res(self):
        """Returns all produced res for whose there is no space"""
        l = []
        for res, amount in self._prod_line.produced_res.iteritems():
            if self.inventory.get_free_space_for(res) < amount:
                l.append(res)
        return l

    ## PROTECTED METHODS
    def _get_producing_callback(self):
        """Returns the callback used during the process of producing (state: producing)"""
        return self._finished_producing

    def _get_first_relevant_tick(self, ignore_pause):
        """
		Returns the first tick that is relevant for production utilisation calculation
		@param ignore_pause: whether to ignore the time spent in the pause state
		"""

        current_tick = Scheduler().cur_tick
        state_hist_len = min(PRODUCTION.STATISTICAL_WINDOW,
                             current_tick - self._creation_tick)

        first_relevant_tick = current_tick - state_hist_len
        if not ignore_pause:
            return first_relevant_tick

        # ignore paused time
        pause_state = PRODUCTION.STATES.paused.index
        for i in xrange(len(self._state_history) - 1, -1, -1):
            if self._state_history[i][1] != pause_state:
                continue
            tick = self._state_history[i][0]
            next_tick = self._state_history[i + 1][0] if i + 1 < len(
                self._state_history) else current_tick
            if next_tick <= first_relevant_tick:
                break
            first_relevant_tick -= next_tick - tick
        return max(self._creation_tick, first_relevant_tick)

    def _clean_state_history(self):
        """ remove the part of the state history that is too old to matter """
        first_relevant_tick = self._get_first_relevant_tick(True)
        while len(self._state_history
                  ) > 1 and self._state_history[1][0] < first_relevant_tick:
            self._state_history.popleft()

    def _changed(self):
        super(Production, self)._changed()
        if not self._prod_line.save_statistics:
            return

        state = self._state.index
        current_tick = Scheduler().cur_tick

        if self._state_history and self._state_history[-1][0] == current_tick:
            self._state_history.pop(
            )  # make sure no two events are on the same tick
        if not self._state_history or self._state_history[-1][1] != state:
            self._state_history.append((current_tick, state))

        self._clean_state_history()

    def _check_inventory(self):
        """Called when assigned building's inventory changed in some way"""
        check_space = self._check_for_space_for_produced_res()
        if not check_space:
            # can't produce, no space in our inventory
            self._state = PRODUCTION.STATES.inventory_full
            self._changed()
        elif self._check_available_res():
            # we have space in our inventory and needed res are available
            # stop listening for res
            self._remove_listeners()
            self._start_production()
        else:
            # we have space in our inventory, but needed res are missing
            self._state = PRODUCTION.STATES.waiting_for_res
            self._changed()

    def _start_production(self):
        """Actually start production. Sets self to producing state"""
        self._state = PRODUCTION.STATES.producing
        self._produce()
        self._changed()

    def _produce(self):
        """Called when there are enough res in the inventory for starting production"""
        self.log.debug("%s _produce", self)
        assert self._check_available_res(
        ) and self._check_for_space_for_produced_res()
        # take the res we need
        self._remove_res_to_expend()
        # call finished in some time
        time = Scheduler().get_ticks(self._prod_line.time)
        Scheduler().add_new_object(self._get_producing_callback(), self, time)
        self.log.debug("%s _produce Adding callback in %d time", self, time)

    def _finished_producing(self, continue_producing=True, **kwargs):
        """Called when the production finishes."""
        self.log.debug("%s finished", self)
        self._give_produced_res()
        self.on_production_finished()
        if continue_producing:
            self._state = PRODUCTION.STATES.waiting_for_res
            self._add_listeners(check_now=True)

    def _add_listeners(self, check_now=False):
        """Listen for changes in the inventory from now on."""
        # don't set call_listener_now to true here, adding/removing changelisteners wouldn't be atomic any more
        self.inventory.add_change_listener(self._check_inventory)
        if self.__class__.uses_gold:
            self.owner_inventory.add_change_listener(self._check_inventory)

        if check_now:  # only check now after adding everything
            self._check_inventory()

    def _remove_listeners(self):
        # depending on state, a check_inventory listener might be active
        self.inventory.discard_change_listener(self._check_inventory)
        if self.__class__.uses_gold:
            self.owner_inventory.discard_change_listener(self._check_inventory)

    def _give_produced_res(self):
        """Put produces goods to the inventory"""
        for res, amount in self._prod_line.produced_res.iteritems():
            self.inventory.alter(res, amount)

    def _check_available_res(self):
        """Checks if all required resources are there.
		@return: bool, true if we can start production
		"""
        for res, amount in self._prod_line.consumed_res.iteritems():
            if self.inventory[res] < (
                    -amount):  # consumed res have negative sign
                return False
        return True

    def _remove_res_to_expend(self):
        """Removes the resources from the inventory, that production takes."""
        for res, amount in self._prod_line.consumed_res.iteritems():
            remnant = self.inventory.alter(res, amount)
            assert remnant == 0

    def _check_for_space_for_produced_res(self):
        """Checks if there is enough space in the inventory for the res, we want to produce.
		@return bool, true if everything can fit."""
        for res, amount in self._prod_line.produced_res.iteritems():
            if self.inventory.get_free_space_for(res) < amount:
                return False
        return True

    def __str__(self):  # debug
        if hasattr(self, "_state"):
            return 'Production(state=%s;prodline=%s)' % (self._state,
                                                         self._prod_line)
        else:

            return "UninitializedProduction()"
	def setUp(self):
		"""Clear ProductionLine cache."""
		ProductionLine.reset()
		super(TestProductionLine, self).setUp()
Exemplo n.º 22
0
class Production(ChangeListener):
	"""Class for production to be used by ResourceHandler.
	Controls production and starts it by watching the assigned building's inventory,
	which is virtually the only "interface" to the building.
	This ensures independence and encapsulation from the building code.

	A Production is active by default, but you can pause it.

	Before we can start production, we check certain assertions, i.e. if we have all the
	resources, that the production takes and if there is enough space to store the produced goods.

	It has basic states, which are useful for e.g. setting animation. Changes in state
	can be observed via ChangeListener interface."""
	log = logging.getLogger('world.production')

	# optimisation:
	# the special resource gold is only stored in the player's inventory.
	# If productions want to use it, they will observer every change of it, which results in
	# a lot calls. Therefore, this is not done by default but only for few subclasses that actually need it.
	uses_gold = False

	keep_original_prod_line = False

	## INIT/DESTRUCT
	def __init__(self, inventory, owner_inventory, prod_id, prod_data, \
	             start_finished=False, load=False, **kwargs):
		"""
		@param inventory: interface to the world, take res from here and put output back there
		@param owner_inventory: same as inventory, but for gold. Usually the players'.
		@param prod_id: int id of the production line
		@param prod_data: ?
		@param start_finished: Whether to start at the final state of a production
		@param load: set to true if this production is supposed to load a saved production
		"""
		super(Production, self).__init__(**kwargs)
		# this has grown to be a bit weird compared to other init/loads
		# __init__ is always called before load, therefore load just overwrites some of the values here
		self._state_history = deque()
		self.prod_id = prod_id
		self.prod_data = prod_data
		self.__start_finished = start_finished
		self.inventory = inventory
		self.owner_inventory = owner_inventory

		self._pause_remaining_ticks = None # only used in pause()
		self._pause_old_state = None # only used in pause()

		self._creation_tick = Scheduler().cur_tick

		assert isinstance(prod_id, int)
		self._prod_line = ProductionLine(id=prod_id, data=prod_data)

		if self.__class__.keep_original_prod_line: # used by unit productions
			self.original_prod_line = self._prod_line.get_original_copy()

		if not load:
			# init production to start right away

			if self.__start_finished:
				# finish the production
				self._give_produced_res()

			self._state = PRODUCTION.STATES.waiting_for_res
			self._add_listeners(check_now=True)

	def save(self, db, owner_id):
		"""owner_id: worldid of the owner of the producer object that owns this production"""
		self._clean_state_history()
		current_tick = Scheduler().cur_tick
		translated_creation_tick = self._creation_tick - current_tick + 1 #  pre-translate the tick number for the loading process

		remaining_ticks = None
		if self._state == PRODUCTION.STATES.paused:
			remaining_ticks = self._pause_remaining_ticks
		elif self._state == PRODUCTION.STATES.producing:
			remaining_ticks = Scheduler().get_remaining_ticks(self, self._get_producing_callback())
		# use a number > 0 for ticks
		if remaining_ticks < 1:
			remaining_ticks = 1
		db('INSERT INTO production(rowid, state, prod_line_id, remaining_ticks, _pause_old_state, creation_tick, owner) VALUES(?, ?, ?, ?, ?, ?, ?)', \
		     None, self._state.index, self._prod_line.id, remaining_ticks, \
			 None if self._pause_old_state is None else self._pause_old_state.index, translated_creation_tick, owner_id)

		# save state history
		for tick, state in self._state_history:
				# pre-translate the tick number for the loading process
			translated_tick = tick - current_tick + 1
			db("INSERT INTO production_state_history(production, tick, state, object_id) VALUES(?, ?, ?, ?)", \
				 self.prod_id, translated_tick, state, owner_id)

	def load(self, db, worldid):
		# NOTE: __init__ must have been called with load=True
		# worldid is the world id of the producer component instance calling this
		super(Production, self).load(db, worldid)

		db_data = db.get_production_by_id_and_owner(self.prod_id, worldid)
		self._creation_tick = db_data[5]
		self._state = PRODUCTION.STATES[db_data[0]]
		self._pause_old_state = None if db_data[4] is None else PRODUCTION.STATES[db_data[4]]
		if self._state == PRODUCTION.STATES.paused:
			self._pause_remaining_ticks = db_data[3]
		elif self._state == PRODUCTION.STATES.producing:
			Scheduler().add_new_object(self._get_producing_callback(), self, db_data[3])
		elif self._state == PRODUCTION.STATES.waiting_for_res or \
				 self._state == PRODUCTION.STATES.inventory_full:
			# no need to call now, this just restores the state before
			# saving , where it hasn't triggered yet, therefore it won't now
			self._add_listeners()

		self._state_history = db.get_production_state_history(worldid, self.prod_id)

	def remove(self):
		self._remove_listeners()
		Scheduler().rem_all_classinst_calls(self)
		super(Production, self).remove()

	## INTERFACE METHODS
	def get_production_line_id(self):
		"""Returns id of production line"""
		return self._prod_line.id

	def get_consumed_resources(self):
		"""Res that are consumed here. Returns dict {res:amount}. Interface for _prod_line."""
		return self._prod_line.consumed_res

	def get_produced_resources(self):
		"""Res that are produced here. Returns dict {res:amount}. Interface for _prod_line."""
		return self._prod_line.produced_res

	def get_production_time(self):
		return self._prod_line.time

	def get_produced_units(self):
		"""@return dict of produced units {unit_id: amount}"""
		return self._prod_line.unit_production

	def changes_animation(self):
		"""Returns whether the production should change the animation"""
		return self._prod_line.changes_animation

	def get_state(self):
		"""Returns the Production's current state"""
		return self._state

	def get_animating_state(self):
		"""Returns the production's current state,
		but only if it effects the animation, else None"""
		if self._prod_line.changes_animation:
			return self._state
		else:
			return None

	def toggle_pause(self):
		if self.is_paused():
			self.pause()
		else:
			self.pause(pause=False)

	def is_paused(self):
		return self._state == PRODUCTION.STATES.paused

	def pause(self, pause = True):
		self.log.debug("Production pause: %s", pause)
		if not pause: # do unpause
			# switch state
			self._state = self._pause_old_state
			self._pause_old_state = None

			# apply state
			if self._state in (PRODUCTION.STATES.waiting_for_res, \
												 PRODUCTION.STATES.inventory_full):
				# just restore watching
				self._add_listeners(check_now=True)

			elif self._state == PRODUCTION.STATES.producing:
				# restore scheduler call
				Scheduler().add_new_object(self._get_producing_callback(), self, \
																   self._pause_remaining_ticks)
			else:
				assert False, 'Unhandled production state: %s' % self._pause_old_state
		else: # do pause
			# switch state
			self._pause_old_state = self._state
			self._state = PRODUCTION.STATES.paused

			if self._pause_old_state in (PRODUCTION.STATES.waiting_for_res, \
												           PRODUCTION.STATES.inventory_full):
				self._remove_listeners()
			elif self._pause_old_state == PRODUCTION.STATES.producing:
				# save when production finishes and remove that call
				self._pause_remaining_ticks = \
						Scheduler().get_remaining_ticks(self, self._get_producing_callback())
				Scheduler().rem_call(self, self._get_producing_callback())
			else:
				assert False, 'Unhandled production state: %s' % self._state

		self._changed()

	def finish_production_now(self):
		"""Makes the production finish now"""
		if self._state != PRODUCTION.STATES.producing:
			return
		Scheduler().rem_call(self, self._get_producing_callback())
		self._finished_producing()

	def alter_production_time(self, modifier):
		"""@see ProductionLine.alter_production_time"""
		try:
			self._prod_line.alter_production_time(modifier)
		except AttributeError: # production line doesn't have this alter method
			pass


	def get_state_history_times(self, ignore_pause):
		"""
		Returns the part of time 0 <= x <= 1 the production has been in a state during the last history_length ticks.
		"""
		self._clean_state_history()
		result = defaultdict(lambda: 0)
		current_tick = Scheduler().cur_tick
		pause_state = PRODUCTION.STATES.paused.index
		first_relevant_tick = self._get_first_relevant_tick(ignore_pause)
		num_entries = len(self._state_history)

		for i in xrange(num_entries):
			if ignore_pause and self._state_history[i][1] == pause_state:
				continue
			tick = self._state_history[i][0]
			if tick >= current_tick:
				break

			next_tick = min(self._state_history[i + 1][0], current_tick) if i + 1 < num_entries else current_tick
			if next_tick <= first_relevant_tick:
				continue
			relevant_ticks = next_tick - tick
			if tick < first_relevant_tick:
				# the beginning is not relevant
				relevant_ticks -= first_relevant_tick - tick
			result[self._state_history[i][1]] += relevant_ticks

		total_length = sum(result.itervalues())
		if total_length == 0:
			return result
		for key in result:
			result[key] /= float(total_length)
		return result

	def get_age(self):
		return Scheduler().cur_tick - self._creation_tick

	def get_unstorable_produced_res(self):
		"""Returns all produced res for whose there is no space"""
		l = []
		for res, amount in self._prod_line.produced_res.iteritems():
			if self.inventory.get_free_space_for(res) < amount:
				l.append(res)
		return l

	## PROTECTED METHODS
	def _get_producing_callback(self):
		"""Returns the callback used during the process of producing (state: producing)"""
		return self._finished_producing

	def _get_first_relevant_tick(self, ignore_pause):
		"""
		Returns the first tick that is relevant for production utilisation calculation
		@param ignore_pause: whether to ignore the time spent in the pause state
		"""

		current_tick = Scheduler().cur_tick
		state_hist_len = min(PRODUCTION.STATISTICAL_WINDOW, current_tick - self._creation_tick)

		first_relevant_tick = current_tick - state_hist_len
		if not ignore_pause:
			return first_relevant_tick

		# ignore paused time
		pause_state = PRODUCTION.STATES.paused.index
		for i in xrange(len(self._state_history) - 1, -1, -1):
			if self._state_history[i][1] != pause_state:
				continue
			tick = self._state_history[i][0]
			next_tick = self._state_history[i + 1][0] if i + 1 < len(self._state_history) else current_tick
			if next_tick <= first_relevant_tick:
				break
			first_relevant_tick -= next_tick - tick
		return max(self._creation_tick, first_relevant_tick)

	def _clean_state_history(self):
		""" remove the part of the state history that is too old to matter """
		first_relevant_tick = self._get_first_relevant_tick(True)
		while len(self._state_history) > 1 and self._state_history[1][0] < first_relevant_tick:
			self._state_history.popleft()

	def _changed(self):
		super(Production, self)._changed()
		if not self._prod_line.save_statistics:
			return

		state = self._state.index
		current_tick = Scheduler().cur_tick

		if self._state_history and self._state_history[-1][0] == current_tick:
			self._state_history.pop() # make sure no two events are on the same tick
		if not self._state_history or self._state_history[-1][1] != state:
			self._state_history.append((current_tick, state))

		self._clean_state_history()

	def _check_inventory(self):
		"""Called when assigned building's inventory changed in some way"""
		check_space = self._check_for_space_for_produced_res()
		if not check_space:
			# can't produce, no space in our inventory
			self._state = PRODUCTION.STATES.inventory_full
			self._changed()
		elif self._check_available_res():
			# we have space in our inventory and needed res are available
			# stop listening for res
			self._remove_listeners()
			self._start_production()
		else:
			# we have space in our inventory, but needed res are missing
			self._state = PRODUCTION.STATES.waiting_for_res
			self._changed()

	def _start_production(self):
		"""Actually start production. Sets self to producing state"""
		self._state = PRODUCTION.STATES.producing
		self._produce()
		self._changed()

	def _produce(self):
		"""Called when there are enough res in the inventory for starting production"""
		self.log.debug("%s _produce", self)
		assert self._check_available_res() and self._check_for_space_for_produced_res()
		# take the res we need
		self._remove_res_to_expend()
		# call finished in some time
		time = Scheduler().get_ticks(self._prod_line.time)
		Scheduler().add_new_object(self._get_producing_callback(), self, time)
		self.log.debug("%s _produce Adding callback in %d time", self, time)

	def _finished_producing(self, continue_producing=True, **kwargs):
		"""Called when the production finishes."""
		self.log.debug("%s finished", self)
		self._give_produced_res()
		self.on_production_finished()
		if continue_producing:
			self._state = PRODUCTION.STATES.waiting_for_res
			self._add_listeners(check_now=True)

	def _add_listeners(self, check_now=False):
		"""Listen for changes in the inventory from now on."""
		# don't set call_listener_now to true here, adding/removing changelisteners wouldn't be atomic any more
		self.inventory.add_change_listener(self._check_inventory)
		if self.__class__.uses_gold:
			self.owner_inventory.add_change_listener(self._check_inventory)

		if check_now: # only check now after adding everything
			self._check_inventory()

	def _remove_listeners(self):
		# depending on state, a check_inventory listener might be active
		self.inventory.discard_change_listener(self._check_inventory)
		if self.__class__.uses_gold:
			self.owner_inventory.discard_change_listener(self._check_inventory)

	def _give_produced_res(self):
		"""Put produces goods to the inventory"""
		for res, amount in self._prod_line.produced_res.iteritems():
			self.inventory.alter(res, amount)

	def _check_available_res(self):
		"""Checks if all required resources are there.
		@return: bool, true if we can start production
		"""
		for res, amount in self._prod_line.consumed_res.iteritems():
			if self.inventory[res] < (-amount): # consumed res have negative sign
				return False
		return True

	def _remove_res_to_expend(self):
		"""Removes the resources from the inventory, that production takes."""
		for res, amount in self._prod_line.consumed_res.iteritems():
			remnant = self.inventory.alter(res, amount)
			assert remnant == 0

	def _check_for_space_for_produced_res(self):
		"""Checks if there is enough space in the inventory for the res, we want to produce.
		@return bool, true if everything can fit."""
		for res, amount in self._prod_line.produced_res.iteritems():
			if self.inventory.get_free_space_for(res) < amount:
				return False
		return True

	def __str__(self): # debug
		if hasattr(self, "_state"):
			return 'Production(state=%s;prodline=%s)' % (self._state, self._prod_line)
		else:

			return "UninitializedProduction()"
Exemplo n.º 23
0
	def _create_production_line(self, prod_line_id):
		"""Returns a changeable production line instance"""
		return ProductionLine(prod_line_id)
Exemplo n.º 24
0
	def _create_production_line(self, prod_line_id):
		"""Returns a non-changeable production line instance"""
		return ProductionLine.get_const_production_line(prod_line_id)
Exemplo n.º 25
0
    def create_production_line(self, id):
        """Creates a production line instance, this is meant only for data transfer and READONLY use!
		If you want to use production lines for anything else, go the proper way of the production class."""
        assert id in self.production_lines
        data = self.production_lines[id]
        return ProductionLine(id, data)