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