Skip to content

Commit

Permalink
Add parameter for chance of random hawk/dove play #45
Browse files Browse the repository at this point in the history
  • Loading branch information
rlskoeser committed Dec 19, 2023
1 parent 9bf82fb commit e6b7f4c
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 14 deletions.
45 changes: 31 additions & 14 deletions simulatingrisk/hawkdove/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
play_choices = [Play.HAWK, Play.DOVE]


# divergent color scheme, nine colors
# from https://colorbrewer2.org/?type=diverging&scheme=RdYlGn&n=9
# divergent color scheme, ten colors
# from https://colorbrewer2.org/?type=diverging&scheme=RdYlGn&n=10
divergent_colors_10 = [
"#a50026",
"#d73027",
Expand Down Expand Up @@ -111,19 +111,33 @@ def proportional_num_dove_neighbors(self):

def choose(self):
"decide what to play this round"
# first choice is random since we don't have any information
# about neighbors' choices
if self.model.schedule.steps == 0:
return

# after the first round, choose based on what neighbors did last time
if self.model.schedule.steps > 0:
# choose based on the number of neighbors who played
# dove last round and agent risk level

# agent with r = 0 should always take the risky choice
# (any risk is acceptable).
# agent with r = max should always take the safe option
# (no risk is acceptable)
if self.proportional_num_dove_neighbors >= self.risk_level:
self.choice = Play.HAWK
else:
self.choice = Play.DOVE

# choose based on the number of neighbors who played
# dove last round and agent risk level

# agent with r = 0 should always take the risky choice
# (any risk is acceptable).
# agent with r = max should always take the safe option
# (no risk is acceptable)
if self.proportional_num_dove_neighbors >= self.risk_level:
choice = Play.HAWK
else:
choice = Play.DOVE

# based on model configuration, should agent play randomly instead?
if self.model.random_play_odds and coinflip(
[True, False], weight=self.model.random_play_odds
):
# if a random play is selected, flip a coin between hawk and dove
choice = coinflip([Play.HAWK, Play.DOVE])

self.choice = choice

def play(self):
# play against each neighbor and calculate cumulative payoff
Expand Down Expand Up @@ -198,6 +212,7 @@ def __init__(
play_neighborhood=8,
observed_neighborhood=8,
hawk_odds=0.5,
random_play_odds=0.01,
):
super().__init__()
# assume a fully-populated square grid
Expand All @@ -210,6 +225,8 @@ def __init__(

# distribution of first choice (50/50 by default)
self.hawk_odds = hawk_odds
# how often should agents make a random play
self.random_play_odds = random_play_odds

# create fifos to track recent behavior to detect convergence
self.recent_percent_hawk = deque([], maxlen=self.rolling_window)
Expand Down
8 changes: 8 additions & 0 deletions simulatingrisk/hawkdove/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ def agent_portrayal(agent):
"max": 1.0,
"step": 0.1,
},
"random_play_odds": {
"type": "SliderFloat",
"value": 0.01,
"label": "Random play odds",
"min": 0.0,
"max": 1.0,
"step": 0.01,
},
}

# in single-risk variant, risk level is set for all agents at init time
Expand Down
29 changes: 29 additions & 0 deletions tests/test_hawkdove.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ def test_agent_choose():
agent = HawkDoveSingleRiskAgent(1, Mock(agent_risk_level=3))
# on the first round, nothing should happen (uses initial choice)
agent.model.schedule.steps = 0
# disable random play for now
agent.model.random_play_odds = 0
agent.choose()

# on subsequent rounds, choose based on neighbors and risk level
Expand Down Expand Up @@ -155,6 +157,33 @@ def test_agent_choose():
assert agent.choice == Play.DOVE


@patch("simulatingrisk.hawkdove.model.coinflip")
def test_agent_choose_random(mock_coinflip):
agent = HawkDoveSingleRiskAgent(1, Mock(agent_risk_level=3))
agent.model.schedule.steps = 1
# reset after init, which calls coinflip for initial play
mock_coinflip.reset_mock()
with patch.object(HawkDoveAgent, "proportional_num_dove_neighbors", 2):
# if random play is disabled, should not flip a coin
agent.model.random_play_odds = 0
agent.choose()
assert mock_coinflip.call_count == 0

# some chance of random play
agent.model.random_play_odds = 0.5
mock_coinflip.side_effect = [True, Play.DOVE]
agent.choose()
# should call twice: once for random play, once for choice
assert mock_coinflip.call_count == 2
# called for random play with model odds
mock_coinflip.assert_any_call(
[True, False], weight=agent.model.random_play_odds
)
# called a second time to determine which play to make
mock_coinflip.assert_any_call([Play.HAWK, Play.DOVE])
assert agent.choice == Play.DOVE


def test_proportional_num_dove_neighbors():
model = HawkDoveSingleRiskModel(4, agent_risk_level=3)
agent = HawkDoveSingleRiskAgent(1, model)
Expand Down

0 comments on commit e6b7f4c

Please sign in to comment.