Skip to content

Commit 3402156

Browse files
Merge pull request #1146 from gaffney2010/master
Implemented Harrington (k75r from Axelrod's Second)
2 parents be2c4b0 + bf2c855 commit 3402156

File tree

5 files changed

+494
-3
lines changed

5 files changed

+494
-3
lines changed

axelrod/strategies/_strategies.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
UnnamedStrategy, SteinAndRapoport, TidemanAndChieruzzi)
1010
from .axelrod_second import (
1111
Champion, Eatherley, Tester, Gladstein, Tranquilizer, MoreGrofman,
12-
Kluepfel, Borufsen, Cave, WmAdams, GraaskampKatzen, Weiner)
12+
Kluepfel, Borufsen, Cave, WmAdams, GraaskampKatzen, Weiner, Harrington)
1313
from .backstabber import BackStabber, DoubleCrosser
1414
from .better_and_better import BetterAndBetter
1515
from .bush_mosteller import BushMosteller
@@ -186,6 +186,7 @@
186186
HardProber,
187187
HardTitFor2Tats,
188188
HardTitForTat,
189+
Harrington,
189190
HesitantQLearner,
190191
Hopeless,
191192
Inverse,

axelrod/strategies/axelrod_second.py

Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,3 +975,333 @@ def strategy(self, opponent: Player) -> Action:
975975
self.defect_padding = 0
976976

977977
return self.try_return(opponent.history[-1])
978+
979+
980+
class Harrington(Player):
981+
"""
982+
Strategy submitted to Axelrod's second tournament by Paul Harrington (K75R)
983+
and came in eighth in that tournament.
984+
985+
This strategy has three modes: Normal, Fair-weather, and Defect. These
986+
mode names were not present in Harrington's submission.
987+
988+
In Normal and Fair-weather modes, the strategy begins by:
989+
990+
- Update history
991+
- Try to detect random opponent if turn is multiple of 15 and >=30.
992+
- Check if `burned` flag should be raised.
993+
- Check for Fair-weather opponent if turn is 38.
994+
995+
Updating history means to increment the correct cell of the `move_history`.
996+
`move_history` is a matrix where the columns are the opponent's previous
997+
move and the rows are indexed by the combo of this player's and the
998+
opponent's moves two turns ago. [The upper-left cell must be all
999+
Cooperations, but otherwise order doesn't matter.] After we enter Defect
1000+
mode, `move_history` won't be used again.
1001+
1002+
If the turn is a multiple of 15 and >=30, then attempt to detect random.
1003+
If random is detected, enter Defect mode and defect immediately. If the
1004+
player was previously in Defect mode, then do not re-enter. The random
1005+
detection logic is a modified Pearson's Chi Squared test, with some
1006+
additional checks. [More details in `detect_random` docstrings.]
1007+
1008+
Some of this player's moves are marked as "generous." If this player made
1009+
a generous move two turns ago and the opponent replied with a Defect, then
1010+
raise the `burned` flag. This will stop certain generous moves later.
1011+
1012+
The player mostly plays Tit-for-Tat for the first 36 moves, then defects on
1013+
the 37th move. If the opponent cooperates on the first 36 moves, and
1014+
defects on the 37th move also, then enter Fair-weather mode and cooperate
1015+
this turn. Entering Fair-weather mode is extremely rare, since this can
1016+
only happen if the opponent cooperates for the first 36 then defects
1017+
unprovoked on the 37th. (That is, this player's first 36 moves are also
1018+
Cooperations, so there's nothing really to trigger an opponent Defection.)
1019+
1020+
Next in Normal Mode:
1021+
1022+
1. Check for defect and parity streaks.
1023+
2. Check if cooperations are scheduled.
1024+
3. Otherwise,
1025+
1026+
- If turn < 37, Tit-for-Tat.
1027+
- If turn = 37, defect, mark this move as generous, and schedule two
1028+
more cooperations**.
1029+
- If turn > 37, then if `burned` flag is raised, then Tit-for-Tat.
1030+
Otherwise, Tit-for-Tat with probability 1 - `prob`. And with
1031+
probability `prob`, defect, schedule two cooperations, mark this move
1032+
as generous, and increase `prob` by 5%.
1033+
1034+
** Scheduling two cooperations means to set `more_coop` flag to two. If in
1035+
Normal mode and no streaks are detected, then the player will cooperate and
1036+
lower this flag, until hitting zero. It's possible that the flag can be
1037+
overwritten. Notable on the 37th turn defect, this is set to two, but the
1038+
38th turn Fair-weather check will set this.
1039+
1040+
If the opponent's last twenty moves were defections, then defect this turn.
1041+
Then check for a parity streak, by flipping the parity bit (there are two
1042+
streaks that get tracked which are something like odd and even turns, but
1043+
this flip bit logic doesn't get run every turn), then incrementing the
1044+
parity streak that we're pointing to. If the parity streak that we're
1045+
pointing to is then greater than `parity_limit` then reset the streak and
1046+
cooperate immediately. `parity_limit` is initially set to five, but after
1047+
it has been hit eight times, it decreases to three. The parity streak that
1048+
we're pointing to also gets incremented if in normal mode and we defect but
1049+
not on turn 38, unless we are defecting as the result of a defect streak.
1050+
Note that the parity streaks resets but the defect streak doesn't.
1051+
1052+
If `more_coop` >= 1, then we cooperate and lower that flag here, in Normal
1053+
mode after checking streaks. Still lower this flag if cooperating as the
1054+
result of a parity streak or in Fair-weather mode.
1055+
1056+
Then use the logic based on turn from above.
1057+
1058+
In Fair-Weather mode after running the code from above, check if opponent
1059+
defected last turn. If so, exit Fair-Weather mode, and proceed THIS TURN
1060+
with Normal mode. Otherwise cooperate.
1061+
1062+
In Defect mode, update the `exit_defect_meter` (originally zero) by
1063+
incrementing if opponent defected last turn and decreasing by three
1064+
otherwise. If `exit_defect_meter` is then 11, then set mode to Normal (for
1065+
future turns), cooperate and schedule two more cooperations. [Note that
1066+
this move is not marked generous.]
1067+
1068+
Names:
1069+
1070+
- Harrington: [Axelrod1980b]_
1071+
"""
1072+
1073+
name = "Harrington"
1074+
classifier = {
1075+
'memory_depth': float('inf'),
1076+
'stochastic': True,
1077+
'makes_use_of': set(),
1078+
'long_run_time': False,
1079+
'inspects_source': False,
1080+
'manipulates_source': False,
1081+
'manipulates_state': False
1082+
}
1083+
1084+
def __init__(self):
1085+
super().__init__()
1086+
self.mode = "Normal"
1087+
self.recorded_defects = 0 # Count opponent defects after turn 1
1088+
self.exit_defect_meter = 0 # When >= 11, then exit defect mode.
1089+
self.coops_in_first_36 = None # On turn 37, count cooperations in first 36
1090+
self.was_defective = False # Previously in Defect mode
1091+
1092+
self.prob = 0.25 # After turn 37, probability that we'll defect
1093+
1094+
self.move_history = np.zeros([4, 2])
1095+
1096+
self.more_coop = 0 # This schedules cooperation for future turns
1097+
# Initial last_generous_n_turns_ago to 3 because this counts up and
1098+
# triggers a strategy change at 2.
1099+
self.last_generous_n_turns_ago = 3 # How many tuns ago was a "generous" move
1100+
self.burned = False
1101+
1102+
self.defect_streak = 0
1103+
self.parity_streak = [0, 0] # Counters that get (almost) alternatively incremented.
1104+
self.parity_bit = 0 # Which parity_streak to increment
1105+
self.parity_limit = 5 # When a parity streak hits this limit, alter strategy.
1106+
self.parity_hits = 0 # Counts how many times a parity_limit was hit.
1107+
# After hitting parity_hits 8 times, lower parity_limit to 3.
1108+
1109+
def try_return(self, to_return, lower_flags=True, inc_parity=False):
1110+
"""
1111+
This will return to_return, with some end-of-turn logic.
1112+
"""
1113+
1114+
if lower_flags and to_return == C:
1115+
# In most cases when Cooperating, we want to reduce the number that
1116+
# are scheduled.
1117+
self.more_coop -= 1
1118+
self.last_generous_n_turns_ago += 1
1119+
1120+
if inc_parity and to_return == D:
1121+
# In some cases we increment the `parity_streak` that we're on when
1122+
# we return a Defection. In detect_parity_streak, `parity_streak`
1123+
# counts opponent's Defections.
1124+
self.parity_streak[self.parity_bit] += 1
1125+
1126+
return to_return
1127+
1128+
def calculate_chi_squared(self, turn):
1129+
"""
1130+
Pearson's Chi Squared statistic = sum[ (E_i-O_i)^2 / E_i ], where O_i
1131+
are the observed matrix values, and E_i is calculated as number (of
1132+
defects) in the row times the number in the column over (total number
1133+
in the matrix minus 1). Equivalently, we expect we expect (for an
1134+
independent distribution) the total number of recorded turns times the
1135+
portion in that row times the portion in that column.
1136+
1137+
In this function, the statistic is non-standard in that it excludes
1138+
summands where E_i <= 1.
1139+
"""
1140+
1141+
denom = turn - 2
1142+
1143+
expected_matrix = np.outer(self.move_history.sum(axis=1),
1144+
self.move_history.sum(axis=0)) / denom
1145+
1146+
chi_squared = 0.0
1147+
for i in range(4):
1148+
for j in range(2):
1149+
expect = expected_matrix[i, j]
1150+
if expect > 1.0:
1151+
chi_squared += (expect - self.move_history[i, j]) ** 2 / expect
1152+
1153+
return chi_squared
1154+
1155+
def detect_random(self, turn):
1156+
"""
1157+
We check if the top-left cell of the matrix (corresponding to all
1158+
Cooperations) has over 80% of the turns. In which case, we label
1159+
non-random.
1160+
1161+
Then we check if over 75% or under 25% of the opponent's turns are
1162+
Defections. If so, then we label as non-random.
1163+
1164+
Otherwise we calculates a modified Pearson's Chi Squared statistic on
1165+
self.history, and returns True (is random) if and only if the statistic
1166+
is less than or equal to 3.
1167+
"""
1168+
1169+
denom = turn - 2
1170+
1171+
if self.move_history[0, 0] / denom >= 0.8:
1172+
return False
1173+
if self.recorded_defects / denom < 0.25 or self.recorded_defects / denom > 0.75:
1174+
return False
1175+
1176+
if self.calculate_chi_squared(turn) > 3:
1177+
return False
1178+
return True
1179+
1180+
def detect_streak(self, last_move):
1181+
"""
1182+
Return true if and only if the opponent's last twenty moves are defects.
1183+
"""
1184+
1185+
if last_move == D:
1186+
self.defect_streak += 1
1187+
else:
1188+
self.defect_streak = 0
1189+
if self.defect_streak >= 20:
1190+
return True
1191+
return False
1192+
1193+
def detect_parity_streak(self, last_move):
1194+
"""
1195+
Switch which `parity_streak` we're pointing to and incerement if the
1196+
opponent's last move was a Defection. Otherwise reset the flag. Then
1197+
return true if and only if the `parity_streak` is at least
1198+
`parity_limit`.
1199+
1200+
This is similar to detect_streak with alternating streaks, except that
1201+
these streaks get incremented elsewhere as well.
1202+
"""
1203+
1204+
self.parity_bit = 1 - self.parity_bit # Flip bit
1205+
if last_move == D:
1206+
self.parity_streak[self.parity_bit] += 1
1207+
else:
1208+
self.parity_streak[self.parity_bit] = 0
1209+
if self.parity_streak[self.parity_bit] >= self.parity_limit:
1210+
return True
1211+
1212+
def strategy(self, opponent: Player) -> Action:
1213+
turn = len(self.history) + 1
1214+
1215+
if turn == 1:
1216+
return C
1217+
1218+
if self.mode == "Defect":
1219+
# There's a chance to exit Defect mode.
1220+
if opponent.history[-1] == D:
1221+
self.exit_defect_meter += 1
1222+
else:
1223+
self.exit_defect_meter -= 3
1224+
# If opponent has been mostly defecting.
1225+
if self.exit_defect_meter >= 11:
1226+
self.mode = "Normal"
1227+
self.was_defective = True
1228+
self.more_coop = 2
1229+
return self.try_return(to_return=C, lower_flags=False)
1230+
1231+
return self.try_return(D)
1232+
1233+
1234+
# If not Defect mode, proceed to update history and check for random,
1235+
# check if burned, and check if opponent's fairweather.
1236+
1237+
# If we haven't yet entered Defect mode
1238+
if not self.was_defective:
1239+
if turn > 2:
1240+
if opponent.history[-1] == D:
1241+
self.recorded_defects += 1
1242+
1243+
# Column decided by opponent's last turn
1244+
history_col = 1 if opponent.history[-1] == D else 0
1245+
# Row is decided by opponent's move two turns ago and our move
1246+
# two turns ago.
1247+
history_row = 1 if opponent.history[-2] == D else 0
1248+
if self.history[-2] == D:
1249+
history_row += 2
1250+
self.move_history[history_row, history_col] += 1
1251+
1252+
# Try to detect random opponent
1253+
if turn % 15 == 0 and turn > 15:
1254+
if self.detect_random(turn):
1255+
self.mode = "Defect"
1256+
return self.try_return(D, lower_flags=False) # Lower_flags not used here.
1257+
1258+
# If generous 2 turns ago and opponent defected last turn
1259+
if self.last_generous_n_turns_ago == 2 and opponent.history[-1] == D:
1260+
self.burned = True
1261+
1262+
# Only enter Fair-weather mode if the opponent Cooperated the first 37
1263+
# turns then Defected on the 38th.
1264+
if turn == 38 and opponent.history[-1] == D and opponent.cooperations == 36:
1265+
self.mode = "Fair-weather"
1266+
return self.try_return(to_return=C, lower_flags=False)
1267+
1268+
1269+
if self.mode == "Fair-weather":
1270+
if opponent.history[-1] == D:
1271+
self.mode = "Normal" # Post-Defect is not possible
1272+
# Proceed with Normal mode this turn.
1273+
else:
1274+
# Never defect against a fair-weather opponent
1275+
return self.try_return(C)
1276+
1277+
# Continue with Normal mode
1278+
1279+
# Check for streaks
1280+
if self.detect_streak(opponent.history[-1]):
1281+
return self.try_return(D, inc_parity=True)
1282+
if self.detect_parity_streak(opponent.history[-1]):
1283+
self.parity_streak[self.parity_bit] = 0 # Reset `parity_streak` when we hit the limit.
1284+
self.parity_hits += 1 # Keep track of how many times we hit the limit.
1285+
if self.parity_hits >= 8: # After 8 times, lower the limit.
1286+
self.parity_limit = 3
1287+
return self.try_return(C, inc_parity=True) # Inc parity won't get used here.
1288+
1289+
# If we have Cooperations scheduled, then Cooperate here.
1290+
if self.more_coop >= 1:
1291+
return self.try_return(C, lower_flags=True, inc_parity=True)
1292+
1293+
if turn < 37:
1294+
# Tit-for-Tat
1295+
return self.try_return(opponent.history[-1], inc_parity=True)
1296+
if turn == 37:
1297+
# Defect once on turn 37 (if no streaks)
1298+
self.more_coop, self.last_generous_n_turns_ago = 2, 1
1299+
return self.try_return(D, lower_flags=False)
1300+
if self.burned or random.random() > self.prob:
1301+
# Tit-for-Tat with probability 1-`prob`
1302+
return self.try_return(opponent.history[-1], inc_parity=True)
1303+
else:
1304+
# Otherwise Defect, Cooperate, Cooperate, and increase `prob`
1305+
self.prob += 0.05
1306+
self.more_coop, self.last_generous_n_turns_ago = 2, 1
1307+
return self.try_return(D, lower_flags=False)

0 commit comments

Comments
 (0)