Skip to content

Two-player games on links and three-body games on 2-simplices

The social dilemmas of Guo et al. on the links and the 2-simplices of a simplicial complex.

  • Name: simplicial_social_dilemmas
  • Field: social/cooperation
  • Formalism: discrete map
  • References: Guo et al. 2021; Majhi et al. 2022

Factory

@register_model(
    field='social/cooperation',
    tags=('simplicial complex', 'games', 'cooperation', 'social dilemmas'),
    references=('Guo et al. 2021', 'Majhi et al. 2022'),
    defaults={'until': 100, 'dt': 1},
)
def simplicial_social_dilemmas(
    n_nodes: int = 500,
    *,
    simplices: float = 0.5,
    sucker: float = -0.2,
    temptation: float = 1.2,
    sucker_allied: float = 0.5,
    temptation_allied: float = 0.8,
    sucker_opposed: float | None = None,
    temptation_opposed: float | None = None,
    noise: float = 0.1,
    share: float = 0.5,
    seed: int = 0,
) -> System:
    """Return the social dilemmas of Guo et al. on the links and the 2-simplices of a simplicial complex.

    The network grows from a complete graph on five nodes, every new node joining both ends of a random link, which
    forms one new triangle. A share `simplices` of the triangles, drawn at random, are 2-simplices, and the others are
    three links. Players play game 1 along the links of a plain triangle, and in a 2-simplex game 2 against a member
    when the third member shares their strategy and game 3 otherwise, every game paying 1 for mutual cooperation, 0
    for mutual defection, and `S` and `T` to a cooperator and a defector that meet. The payoff of a player is the mean
    over the two links of every triangle around it. Every step has two phases: in `play` every player takes its
    payoff, and in `imitate` every player copies a random neighbour with the Fermi probability `1 / (1 + exp((P_own -
    P_other) / K))` at the noise `K`.

    The review of Majhi et al. writes the payoff as a mean over the links of a player of the mean over the triangles of
    each link, and picks game 2 when `s_ki = s_kj`. This model takes the mean over the links of the triangles, which
    counts a link once per triangle, and reads the condition as the third member sharing the strategy of the player.

    Args:
        n_nodes:
            The number of players.

        simplices:
            The share of the triangles that are 2-simplices.

        sucker:
            The sucker's payoff `S1` of game 1.

        temptation:
            The temptation `T1` of game 1.

        sucker_allied:
            The sucker's payoff `S2` of game 2.

        temptation_allied:
            The temptation `T2` of game 2.

        sucker_opposed:
            The sucker's payoff `S3` of game 3, or `None` for `S1`.

        temptation_opposed:
            The temptation `T3` of game 3, or `None` for `T1`.

        noise:
            The noise of the Fermi imitation.

        share:
            The share of cooperators at the start.

        seed:
            The seed of the network, the 2-simplices, and the initial strategies.

    Returns:
        system:
            The model, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.models.social.cooperation import simplicial_social_dilemmas
        >>> simplicial_social_dilemmas(30).elements[0].check().prepare().size_
        30
    """
    rng = np.random.default_rng(seed)
    graph, triangles = _triangle_attachment(n_nodes, rng)
    filled = rng.random(len(triangles)) < simplices
    hyperedges: list[tuple[int, ...]] = []
    for triangle, simplex in zip(triangles, filled, strict=True):
        hyperedges.extend([triangle] if simplex else itertools.combinations(triangle, PAIR))
    held = np.bincount(np.asarray(triangles).ravel(), minlength=n_nodes).astype(float)
    players = LatticeElement(
        'players',
        element=DilemmaElement,
        space=NetworkSpace(graph, hyperedges=hyperedges),
        values={'strategy': (rng.random(n_nodes) < share).astype(float), 'triangles': held},
        visual=VisualSpec('sites', {'states': ['strategy'], 'low': 0.0, 'high': 1.0}),
    )
    play = SimplicialDilemmaInteraction(
        'play',
        target='players',
        sucker=sucker,
        temptation=temptation,
        sucker_allied=sucker_allied,
        temptation_allied=temptation_allied,
        sucker_opposed=sucker if sucker_opposed is None else sucker_opposed,
        temptation_opposed=temptation if temptation_opposed is None else temptation_opposed,
        phase='play',
    )
    imitation = ImitationInteraction(
        'imitation',
        target='players',
        state='strategy',
        rule='fermi',
        form='probability',
        intensity=1 / noise,
        phase='imitate',
    )
    time = DiscreteTime(schedule=PhasesScheduleSpec(tuple(PhaseSpec(phase) for phase in PHASES)))
    return System(elements=[players], interactions=[play, imitation], time=time)

Components

class DilemmaElement(Element):
    """Player of the social dilemmas on a simplicial complex, a cooperator at 1 and a defector at 0, with its payoff.

    The play is a `SimplicialDilemmaInteraction` of the network, and the imitation an `ImitationInteraction` that
    reads the payoffs of the same step, which `simplicial_social_dilemmas` adds.

    Args:
        label:
            The label of the player or the group.

        visual:
            How the player is drawn, or `None` for `default_visual_`.

        strategy:
            The initial strategy, 1 or 0.

        payoff:
            The payoff of the last play, the mean payoff of the player over the links of its triangles.

        triangles:
            The number of triangles of the network that hold the player.

    Examples:
        >>> from skcomplex.models.social.cooperation import DilemmaElement
        >>> DilemmaElement('player').triangles
        1.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        strategy: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
        payoff: Annotated[float, StateSpec()] = 0.0,
        triangles: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.strategy = strategy
        self.payoff = payoff
        self.triangles = triangles

class SimplicialDilemmaInteraction(CouplingInteraction):
    """Play of two-player games along the links of the triangles of a simplicial complex, which sets the payoffs.

    It is a `CouplingInteraction` over the hyperedges of a network in the map form, a cooperator at 1 and a defector
    at 0. Every game pays 1 for mutual cooperation, 0 for mutual defection, and the sucker's payoff `S` and the
    temptation `T` when the strategies differ. A triangle of three links is three hyperedges of two members, along
    which the players play game 1 of `S1` and `T1`. A triangle filled as a 2-simplex is one hyperedge of three members,
    where a player plays each other member while the third looks on: game 2 of `S2` and `T2` when the third member
    shares the player's strategy, and game 3 of `S3` and `T3` otherwise. The payoff of a player is the sum over the two
    links of every triangle around it divided by twice its number of triangles, a parameter of the players.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the players on the network.

        sucker:
            The payoff `S1` of a cooperator against a defector along a link.

        temptation:
            The payoff `T1` of a defector against a cooperator along a link.

        sucker_allied:
            The payoff `S2` in a 2-simplex whose third member shares the player's strategy.

        temptation_allied:
            The payoff `T2` in a 2-simplex whose third member shares the player's strategy.

        sucker_opposed:
            The payoff `S3` in a 2-simplex whose third member holds the other strategy.

        temptation_opposed:
            The payoff `T3` in a 2-simplex whose third member holds the other strategy.

        phase:
            The phase of the step the play runs in, or `None` for the default phase.

    Examples:
        >>> from skcomplex.models.social.cooperation import SimplicialDilemmaInteraction
        >>> SimplicialDilemmaInteraction('play', target='players', temptation_allied=0.8).check().temptation_allied
        0.8
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        sucker: Annotated[float, ParameterSpec()] = -0.2,
        temptation: Annotated[float, ParameterSpec()] = 1.2,
        sucker_allied: Annotated[float, ParameterSpec()] = 0.5,
        temptation_allied: Annotated[float, ParameterSpec()] = 0.8,
        sucker_opposed: Annotated[float, ParameterSpec()] = -0.2,
        temptation_opposed: Annotated[float, ParameterSpec()] = 1.2,
        phase: str | None = None,
    ) -> None:
        super().__init__(
            label,
            target=target,
            mode='hyperedges',
            form='map',
            write='payoff',
            function=self._get_games,
            combine=self._get_mean,
            phase=phase,
        )
        self.sucker = sucker
        self.temptation = temptation
        self.sucker_allied = sucker_allied
        self.temptation_allied = temptation_allied
        self.sucker_opposed = sucker_opposed
        self.temptation_opposed = temptation_opposed

    def _get_games(self, hyperedge: SimpleNamespace) -> Expression:
        """Return the payoff of a member from the games along its links in one hyperedge of two or three members.

        In a link a cooperator earns 1 or `S1` and a defector `T1` or 0 against a cooperator or a defector. In a
        2-simplex with `c` cooperating others a cooperator earns 2, `1 + S2`, or `2 S3`, and a defector `2 T3`, `T2`, or
        0, for `c` of 2, 1, or 0.
        """
        own = hyperedge.member.strategy
        others = total(hyperedge.members.strategy, over=hyperedge.members) - own
        link = own * (others + (1 - others) * hyperedge.sucker) + (1 - own) * others * hyperedge.temptation
        cooperator = where(
            others == PAIR,
            PAIR,
            where(others == 1, 1 + hyperedge.sucker_allied, PAIR * hyperedge.sucker_opposed),
        )
        defector = where(
            others == PAIR,
            PAIR * hyperedge.temptation_opposed,
            where(others == 1, hyperedge.temptation_allied, 0),
        )
        simplex = own * cooperator + (1 - own) * defector
        return where(count(over=hyperedge.members) == PAIR, link, simplex)

    @staticmethod
    def _get_mean(player: SimpleNamespace, games: Expression) -> Expression:
        """Return the payoffs of the games of a player divided by twice its number of triangles."""
        return games / (PAIR * player.member.triangles)

class ImitationInteraction(NeighborInteraction):
    """Imitation of a discrete state of the neighbours of every member, at a rate or with a probability per step.

    By the rule `'random'` a member copies a neighbour chosen at random, by `'majority'` it takes the option that more
    of its neighbours hold than any other, and by `'best'` it copies the neighbour whose payoff is largest, when that
    payoff is larger than its own. By `'better'` it picks a neighbour at random and copies it when the neighbour's
    payoff is larger than its own. By `'fermi'` it picks a neighbour at random and copies it with probability
    `1 / (1 + exp(-beta (P_j - P_i)))`, where `P_j` and `P_i` are the payoffs of the neighbour and of the member and
    `beta` is the intensity. A member without such a neighbour keeps its state. The rules read the neighbourhood of
    the space, the graph of a network or the neighbourhood of a lattice.

    In the `'rate'` form every member imitates at the rate `rate`, by jumps between every pair of options in continuous
    time. In the `'probability'` form every member imitates with probability `probability` in every step of a
    discrete-time system, all members at once in its `phase`, reading the states the phases before it set. In the
    `'agent'` form the target is an agent group, and every agent imitates its neighbours in the space with probability
    `probability` in its `phase`. The agents imitate at once, reading the states of the step before, under the schedule
    `'synchronous'`, one after the other in their order or in a new random order every step under `'sequential'` or
    `'random-sequential'`, each reading the states the agents before it set, or each in its own activation under
    `'member'`. The rule `'better'` runs in the agent form only. Among neighbours of equal largest payoff, the rule
    `'best'` of the agent form copies the first under the ties `'first'` and one drawn uniformly under `'random'`.

    Args:
        label:
            The label of the interaction, unique among the parts of its system.

        target:
            The path of the group on the nodes or sites.

        state:
            The name of the discrete state of the members.

        payoff:
            The name of the state that holds the payoff of the members, which the rules `'best'` and `'fermi'` read.

        options:
            The values the state takes.

        rate:
            The rate at which every member imitates, in the rate form.

        rule:
            `'random'`, `'majority'`, `'best'`, or `'fermi'`.

        intensity:
            The intensity `beta` of the Fermi rule.

        form:
            `'rate'` for imitation in continuous time, `'probability'` for imitation in discrete time, or `'agent'`
            for imitation of the neighbours of the agents of an agent group.

        probability:
            The probability that a member imitates in a step, in the probability form.

        layer:
            The layer of the network whose edges the interaction runs on, or `None` for the graph of the network.

        phase:
            The phase of the step the agent and the probability forms run in, or `None` for the default phase.

        schedule:
            The order of the agents in the agent form, `'synchronous'`, `'sequential'`, `'random-sequential'`, or
            `'member'`.

        ties:
            The neighbour the rule `'best'` of the agent form copies among ties, `'first'` or `'random'`.

    Raises:
        ValidationError:
            The rule, the form, the schedule, or the ties are unknown, a schedule or ties other than the defaults are
            given outside the agent form, or the options are not distinct.

    Examples:
        >>> from skcomplex.core.interactions import ImitationInteraction
        >>> ImitationInteraction('imitation', target='voters', rule='majority').check().options
        (0.0, 1.0)
        >>> ImitationInteraction('imitation', target='players', rule='fermi', form='probability').check().form
        'probability'
        >>> ImitationInteraction('imitation', target='traders', rule='better', form='agent', phase='learn').check().rule
        'better'
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        state: str = 'state',
        payoff: str = 'payoff',
        options: Sequence[float] = (0.0, 1.0),
        rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        rule: str = 'random',
        intensity: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        form: str = 'rate',
        probability: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
        layer: str | None = None,
        phase: str | None = None,
        schedule: str = 'synchronous',
        ties: str = 'first',
    ) -> None:
        super().__init__(label, layer=layer)
        self.target = target
        self.state = state
        self.payoff = payoff
        self.options = options
        self.rate = rate
        self.rule = rule
        self.intensity = intensity
        self.form = form
        self.probability = probability
        self.phase = phase
        self.schedule = schedule
        self.ties = ties

    def _check_agent_options(self) -> None:
        """Raise an error for an unknown schedule or ties, or either of them not default outside the agent form."""
        self._check_variant('schedule', self.schedule, SCHEDULES)
        self._check_variant('ties', self.ties, TIES)
        if self.form != 'agent' and (self.schedule != 'synchronous' or self.ties != 'first'):
            error_msg = (
                f'The imitation {self.label!r} has the schedule {self.schedule!r} and the ties {self.ties!r}, which '
                'only the agent form runs. Use form="agent", or schedule="synchronous" and ties="first".'
            )
            raise ValidationError(error_msg)

    def _check(self) -> None:
        """Raise an error for an unknown rule, form, schedule, or ties, or options that are not distinct.

        Raises:
            ValidationError:
                The rule, the form, the schedule, or the ties are unknown, a schedule or ties other than the defaults
                are given outside the agent form, or the options are not distinct.
        """
        super()._check()
        self._check_variant('rule', self.rule, RULES, (BaseSpec,))
        if self.form not in FORMS:
            error_msg = f'The form of imitation is {self.form!r}. Use one of {", ".join(FORMS)}.'
            raise ValidationError(error_msg)
        if self.rule == 'better' and self.form != 'agent':
            error_msg = f'The rule of imitation {self.rule!r} runs in the agent form only. Use form `agent`.'
            raise ValidationError(error_msg)
        self._check_agent_options()
        if len(set(self.options)) < max(len(self.options), 2):
            error_msg = f'The options of imitation are {self.options!r}. Give at least two distinct values.'
            raise ValidationError(error_msg)

    def _get_neighborhood(self) -> AdjacencyNeighborhood | None:
        """Return the neighbourhood of the layer, or `None` for the neighbourhood of the space."""
        return None if self.layer is None else AdjacencyNeighborhood(layer=self.layer)

    def _get_fermi(self, member: SimpleNamespace, other: SimpleNamespace, intensity: Expression) -> Expression:
        """Return the Fermi probability that a member copies another of a different payoff."""
        return 1 / (1 + exp(-intensity * (getattr(other, self.payoff) - getattr(member, self.payoff))))

    def _get_counts(self, namespace: SimpleNamespace) -> dict[float, Expression]:
        """Return the number of the neighbours of the target that hold every option."""
        source, over = self._get_member(namespace, 'source'), self._get_neighborhood()
        return {
            option: neighbors(where(getattr(source, self.state) == option, 1, 0), over=over) for option in self.options
        }

    def _get_majority(self, namespace: SimpleNamespace) -> dict[float, Expression]:
        """Return the indicator of every option that more of the neighbours of the target hold than any other."""
        counts = self._get_counts(namespace)
        leads = {}
        for option, count in counts.items():
            lead = SR(1)
            for other, other_count in counts.items():
                if other != option:
                    lead = lead * where(count > other_count, 1, 0)
            leads[option] = lead
        return leads

    def _get_best(self, namespace: SimpleNamespace) -> tuple[Expression, Expression]:
        """Return the option of the neighbour of largest payoff and whether that payoff is larger than the target's."""
        source, over = self._get_member(namespace, 'source'), self._get_neighborhood()
        payoff = getattr(source, self.payoff)
        best = neighbors(getattr(source, self.state), over=over, reduce='argmax', weight=payoff)
        largest = neighbors(payoff, over=over, reduce='max')
        return best, where(largest > getattr(self._get_member(namespace, 'target'), self.payoff), 1, 0)

    def _switches(self, value: Expression, rate: Expression, chosen: Expression) -> list[BaseLaw]:
        """Return the jumps of a state from every option to the chosen option, at a rate when it is chosen."""
        return [
            JumpLaw(rate * where(value == first, 1, 0) * where(chosen == second, 1, 0), {value: second - first})
            for first, second in itertools.permutations(self.options, 2)
        ]

    def _copies(self, member: SimpleNamespace, other: SimpleNamespace, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the copies of the state of the other end of an edge by a member, chosen among its neighbours."""
        value, rate = getattr(member, self.state), self._get_parameter_symbol('rate', namespace) / member.degree
        if self.rule == 'fermi':
            rate = rate * self._get_fermi(member, other, self._get_parameter_symbol('intensity', namespace))
        return self._switches(value, rate, getattr(other, self.state))

    def _get_jumps(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the jumps of the state at the rate of imitation, in continuous time."""
        value = getattr(self._get_member(namespace, 'target'), self.state)
        if self.rule == 'majority':
            leads = self._get_majority(namespace)
            return [
                JumpLaw(
                    self._get_parameter_symbol('rate', namespace) * where(value == first, 1, 0) * leads[second],
                    {value: second - first},
                )
                for first, second in itertools.permutations(self.options, 2)
            ]
        if self.rule == 'best':
            best, better = self._get_best(namespace)
            return self._switches(value, self._get_parameter_symbol('rate', namespace) * better, best)
        if self.rule not in ('random', 'fermi'):
            raise self._get_variant_error(self.rule, 'rule', '_get_jumps')
        source, target = self._get_member(namespace, 'source'), self._get_member(namespace, 'target')
        return [*self._copies(target, source, namespace), *self._copies(source, target, namespace)]

    def _draw_copy(self, namespace: SimpleNamespace) -> Expression:
        """Return the next state of the target by the random or the Fermi rule, from the option it copies or its own."""
        source, target = self._get_member(namespace, 'source'), self._get_member(namespace, 'target')
        value = getattr(target, self.state)
        probability, over = self._get_parameter_symbol('probability', namespace), self._get_neighborhood()
        counts = self._get_counts(namespace)
        degree = sum(counts.values(), SR(0))
        if self.rule == 'fermi':
            fermi = self._get_fermi(target, source, self._get_parameter_symbol('intensity', namespace))
            weights = [
                probability * neighbors(where(getattr(source, self.state) == option, 1, 0) * fermi, over=over)
                for option in self.options
            ]
        else:
            weights = [probability * counts[option] for option in self.options]
        stay = degree - sum(weights, SR(0)) + where(degree == 0, 1, 0)
        index = categorical(f'{value}_imitation', [stay, *weights])
        return value + sum(
            (where(index == position + 1, option - value, 0) for position, option in enumerate(self.options)),
            SR(0),
        )

    def _get_update(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the update of the state of the target with the probability of imitation, in discrete time."""
        value = getattr(self._get_member(namespace, 'target'), self.state)
        if self.rule in ('random', 'fermi'):
            return [UpdateLaw(value, self._draw_copy(namespace), phase=self.phase)]
        if self.rule == 'majority':
            leads = self._get_majority(namespace)
            imitated = value + sum((lead * (option - value) for option, lead in leads.items()), SR(0))
        elif self.rule != 'best':
            raise self._get_variant_error(self.rule, 'rule', '_get_update')
        else:
            best, better = self._get_best(namespace)
            imitated = where(better == 1, best, value)
        return [
            UpdateLaw(
                value,
                where(
                    uniform(f'{value}_imitation') < self._get_parameter_symbol('probability', namespace),
                    imitated,
                    value,
                ),
                phase=self.phase,
            ),
        ]

    def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the jumps or the update of the state by the rule of the imitation.

        Args:
            namespace:
                The namespace of an edge.

        Returns:
            laws:
                The jumps of the state between every pair of options, or the update of the state of every member.
        """
        if self.form == 'agent' and self.schedule == 'member':
            return [RuleLaw(self._imitate_member, phase=self.phase, members=self._get_role('target'))]
        if self.form == 'agent':
            return [RuleLaw(self._imitate, phase=self.phase)]
        if self.form == 'probability':
            return self._get_update(namespace)
        return self._get_jumps(namespace)

    def _choose(
        self,
        agent: AgentViewProtocol,
        neighbours: Sequence[AgentViewProtocol],
        model: ModelViewProtocol,
    ) -> float | None:
        """Return the option an agent copies from its neighbours by the rule, or `None` to keep its own."""
        rng = model.rng
        if self.rule == 'majority':
            counts = [sum(getattr(other, self.state) == option for other in neighbours) for option in self.options]
            leaders = np.flatnonzero(np.asarray(counts) == max(counts))
            return float(self.options[int(leaders[0])]) if len(leaders) == 1 else None
        if self.rule == 'random':
            return float(getattr(neighbours[int(rng.integers(len(neighbours)))], self.state))
        own = float(getattr(agent, self.payoff))
        if self.rule == 'best':
            payoffs = np.array([getattr(other, self.payoff) for other in neighbours], dtype=float)
            best = self._pick_best(payoffs, self.ties, model)
            return float(getattr(neighbours[best], self.state)) if payoffs[best] > own else None
        other = neighbours[int(rng.integers(len(neighbours)))]
        difference = float(getattr(other, self.payoff)) - own
        if self.rule == 'better':
            return float(getattr(other, self.state)) if difference > 0 else None
        if self.rule != 'fermi':
            raise self._get_variant_error(self.rule, 'rule', '_choose')
        fermi = 1 / (1 + np.exp(-self._get_parameter_value('intensity', model) * difference))
        return float(getattr(other, self.state)) if rng.random() < fermi else None

    def _get_choice(self, agent: AgentViewProtocol, model: ModelViewProtocol) -> float | None:
        """Return the option an agent copies with the probability of imitation, or `None` to keep its own."""
        groups = set(model.groups(self._get_role('target')))
        neighbours = [other for other in agent.neighbors(layer=self.layer) if other.group in groups]
        if not neighbours or model.rng.random() >= self._get_parameter_value('probability', model):
            return None
        return self._choose(agent, neighbours, model)

    def _set_choice(self, agent: AgentViewProtocol, choice: float | None) -> None:
        """Set the state of an agent to the option it copies, if any."""
        if choice is not None:
            setattr(agent, self.state, choice)

    def _imitate_member(self, agent: AgentViewProtocol, model: ModelViewProtocol) -> None:
        """Let one agent imitate its neighbours with the probability of imitation, in its own activation."""
        if agent.alive:
            self._set_choice(agent, self._get_choice(agent, model))

    def _imitate(self, model: ModelViewProtocol) -> None:
        """Let every agent imitate its neighbours with the probability of imitation, by the schedule."""
        self._update_agents(model, self.schedule, self._get_choice, self._set_choice)

Equations

players.strategy[i]: strategy_players(t + 1) == strategy_players(t) + where(where((_neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0))*categorical_strategy_players__target_imitation_imitation >= -_neighbours_4*probability_imitation + _neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0), 1, 0) + where((_neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0))*categorical_strategy_players__target_imitation_imitation >= -_neighbours_4*probability_imitation - _neighbours_5*probability_imitation + _neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0), 1, 0) == 1, -strategy_players(t), 0) + where(where((_neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0))*categorical_strategy_players__target_imitation_imitation >= -_neighbours_4*probability_imitation + _neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0), 1, 0) + where((_neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0))*categorical_strategy_players__target_imitation_imitation >= -_neighbours_4*probability_imitation - _neighbours_5*probability_imitation + _neighbours_2 + _neighbours_3 + where(_neighbours_2 + _neighbours_3 == 0, 1, 0), 1, 0) == 2, -strategy_players(t) + 1.0, 0)
players.payoff[i]: payoff_players(t + 1) == 1/2*(_neighbours_0 + _neighbours_1)/triangles_players