Skip to content

Commit 6b24094

Browse files
committed
Implemented Harrington (k75r from Axelrod's Second)
1 parent 3d61cbf commit 6b24094

File tree

5 files changed

+420
-3
lines changed

5 files changed

+420
-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: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,3 +975,294 @@ 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 Harringtn (K75R),
983+
and came in eighth in that tournament.
984+
985+
This strategy has three modes: Normal, and Fair-weather, Defect.
986+
987+
In Normal and Fair-weather modes, the strategy begins by:
988+
989+
- Update history
990+
- Detects random if turn is multiple of 15 and >=30.
991+
- Check if `burned` flag should be raised.
992+
- Check for Fair-weather opponent if turn is 38.
993+
994+
Updating history means to increment the correct cell of the `move_history`.
995+
`move_history` is a matrix where the columns are the opponent's previous
996+
move and rows are indexed by the combo of this player and the opponent's
997+
moves two turns ago*. [The upper-left cell must be all cooperations, but
998+
otherwise order doesn't matter.] * If the player is exiting Defect mode,
999+
then the history to determine the row is taken from before the turn that
1000+
the player entered Defect mode. (That is, the turn that started in Normal
1001+
mode, but ended in Defect mode.)
1002+
1003+
If the turn is a multiple of 15 and >=30, then attempt to detect random.
1004+
If random is detected, enter Defect mode and defect immediately. If the
1005+
player was previously in Defect mode, then do not re-enter. The random
1006+
detection logic is a modified Pearson's Chi Squared test, with some
1007+
additional checks. [More details in `detect_random` docstrings.]
1008+
1009+
Some of this player's moves are marked as "generous." If this player made
1010+
a generous move two turns ago and the opponent replied with a Defect, then
1011+
raise the `burned` flag. This will stop certain generous moves later.
1012+
1013+
The player mostly plays Tit-for-Tat for the first 36 moves, then defects on
1014+
the 37th move. If the opponent cooperates on the first 36 moves, and
1015+
defects on the 37th move also, then enter Fair-weather mode and cooperate
1016+
this turn.
1017+
1018+
Next in Normal Mode:
1019+
1020+
1. Check for defect and parity streaks.
1021+
2. Check if cooperations are scheduled.
1022+
3. Otherwise,
1023+
- If turn < 37, Tit-for-Tat.
1024+
- If turn = 37, defect, mark this move as generous, and schedule two
1025+
more cooperations**.
1026+
- If turn > 37, then if `burned` flag is raised, then Tit-for-Tat.
1027+
Otherwise, Tit-for-Tat with probability 1 - `prob`. And with
1028+
probability `prob`, defect, schedule two cooperations, mark this move
1029+
as generous, and increase `prob` by 5%.
1030+
1031+
** Scheduling two cooperations means to set `more_coop` flag to two. If in
1032+
Normal mode and no streaks are detected, then the player will cooperate and
1033+
lower this flag, until hitting zero. It's possible that the flag can be
1034+
overwritten. Notable on the 37th turn defect, this is set to two, but the
1035+
38th turn Fair-weather check will set this.
1036+
1037+
If the opponent's last twenty moves were defections, then defect this turn.
1038+
Then check for a parity streak, by flipping the parity bit (there are two
1039+
streaks that get tracked which are something like odd and even turns, but
1040+
this flip bit logic doesn't get run every turn), then incrementing the
1041+
parity streak that we're pointing to. If the parity streak that we're
1042+
pointing to is then greater than `parity_limit` then reset the streak and
1043+
cooperate immediately. `parity_limit` is initially set to five, but after
1044+
its been hit eight times, it decreases to three. The parity streak that
1045+
we're pointing to also gets incremented if in normal mode and WE defect but
1046+
not on turn 38, unless the result of a defect streak. Note that the parity
1047+
streaks reset but the defect streak doesn't.
1048+
1049+
If `more_coop` >= 1, then we cooperate and lower that flag here, in Normal
1050+
mode after checking streaks. Still lower this flag if cooperating as the
1051+
result of a parity streak or in Fair-weather mode.
1052+
1053+
Then use the logic based on turn from above.
1054+
1055+
In Fair-Weather mode after running the code from above, check if opponent
1056+
defected last turn. If so, exit Fair-Weather mode, and proceed THIS TURN
1057+
with Normal mode. Otherwise cooperate.
1058+
1059+
In Defect mode, update the `exit_defect_meter` (originally zero) by
1060+
incrementing if opponent defected last turn and decreasing by three
1061+
otherwise. If `exit_defect_meter` is then 11, then set mode to Normal (for
1062+
future turns), cooperate and schedule two more cooperations. [Note that
1063+
this move is not marked generous.]
1064+
1065+
Names:
1066+
1067+
- Harrington: [Axelrod1980b]_
1068+
"""
1069+
1070+
name = "Harrington"
1071+
classifier = {
1072+
'memory_depth': float('inf'),
1073+
'stochastic': True,
1074+
'makes_use_of': set(),
1075+
'long_run_time': False,
1076+
'inspects_source': False,
1077+
'manipulates_source': False,
1078+
'manipulates_state': False
1079+
}
1080+
1081+
def __init__(self):
1082+
super().__init__()
1083+
self.mode = "Normal"
1084+
self.recorded_defects = 0
1085+
self.exit_defect_meter = 0
1086+
self.coops_in_first_36 = None
1087+
self.was_defective = False
1088+
1089+
self.prob = 0.25
1090+
1091+
self.move_history = np.zeros([4, 2])
1092+
self.history_row = 0
1093+
1094+
self.more_coop = 0
1095+
self.generous_n_turns_ago = 3
1096+
self.burned = False
1097+
1098+
self.defect_streak = 0
1099+
self.parity_streak = [0, 0]
1100+
self.parity_bit = 0
1101+
self.parity_limit = 5
1102+
self.parity_hits = 0
1103+
1104+
def try_return(self, to_return, lower_flags=True, inc_parity=False):
1105+
if lower_flags and to_return == C:
1106+
self.more_coop -= 1
1107+
self.generous_n_turns_ago += 1
1108+
1109+
if inc_parity and to_return == D:
1110+
self.parity_streak[self.parity_bit] += 1
1111+
1112+
return to_return
1113+
1114+
def detect_random(self, turn):
1115+
"""
1116+
Calculates a modified Pearson's Chi Squared statistic on self.history,
1117+
and returns True (is random) if and only if the statistic is less than
1118+
or equal to 3.
1119+
1120+
Pearson's Chi Squared statistic = sum[ (E_i-O_i)^2 / E_i ], where O_i
1121+
are the observed matrix values, and E_i is calculated as number (of
1122+
defects) in the row times the number in the column over (total number
1123+
in the matrix minus 1).
1124+
1125+
We say this is modified because it differs from a usual Chi-Squared
1126+
test in that:
1127+
1128+
- It divides by turns minus 2 to get expected, whereas usually we'd
1129+
divide by matrix total. Total equals turns minus 1, unless Defect
1130+
mode has been entered at any point.
1131+
- Terms where expected counts are less than 1 get excluded.
1132+
- There's a check at the beginning on the first cell of the matrix.
1133+
- There's a check at the beginning for the recorded number of defects.
1134+
1135+
"""
1136+
denom = turn - 2
1137+
1138+
if self.move_history[0, 0] / denom >= 0.8:
1139+
return False
1140+
if self.recorded_defects / denom < 0.25 or self.recorded_defects / denom > 0.75:
1141+
return False
1142+
1143+
expected_matrix = np.outer(self.move_history.sum(axis=1), \
1144+
self.move_history.sum(axis=0))
1145+
1146+
chi_squared = 0.0
1147+
for i in range(4):
1148+
for j in range(2):
1149+
expct = expected_matrix[i, j] / denom
1150+
if expct > 1.0:
1151+
chi_squared += (expct - self.move_history[i, j]) ** 2 / expct
1152+
1153+
if chi_squared > 3:
1154+
return False
1155+
return True
1156+
1157+
def detect_streak(self, last_move):
1158+
"""
1159+
Return if and only if the opponent's last twenty moves are defects.
1160+
"""
1161+
1162+
if last_move == D:
1163+
self.defect_streak += 1
1164+
else:
1165+
self.defect_streak = 0
1166+
if self.defect_streak >= 20:
1167+
return True
1168+
return False
1169+
1170+
def detect_parity_streak(self, last_move):
1171+
self.parity_bit = 1 - self.parity_bit # Flip bit
1172+
if last_move == D:
1173+
self.parity_streak[self.parity_bit] += 1
1174+
else:
1175+
self.parity_streak[self.parity_bit] = 0
1176+
if self.parity_streak[self.parity_bit] >= self.parity_limit:
1177+
return True
1178+
1179+
def strategy(self, opponent: Player) -> Action:
1180+
turn = len(self.history) + 1
1181+
1182+
if turn == 1:
1183+
return C
1184+
1185+
if self.mode == "Defect":
1186+
if opponent.history[-1] == D:
1187+
self.exit_defect_meter += 1
1188+
else:
1189+
self.exit_defect_meter -= 3
1190+
if self.exit_defect_meter >= 11:
1191+
self.mode = "Normal"
1192+
self.was_defective = True
1193+
self.more_coop = 2
1194+
return self.try_return(to_return=C, lower_flags=False)
1195+
1196+
return self.try_return(D)
1197+
1198+
1199+
# If not Defect mode, proceed to update history and check for random,
1200+
# check if burned, and check if opponent's fairweather.
1201+
1202+
# History only gets updated outside of Defect mode.
1203+
if turn > 2:
1204+
if opponent.history[-1] == D:
1205+
self.recorded_defects += 1
1206+
opp_col = 1 if opponent.history[-1] == D else 0
1207+
self.move_history[self.history_row, opp_col] += 1
1208+
1209+
# Detect random
1210+
if turn % 15 == 0 and turn > 15 and not self.was_defective:
1211+
if self.detect_random(turn):
1212+
self.mode = "Defect"
1213+
return self.try_return(D, lower_flags=False) # Lower_flags not used here.
1214+
1215+
# history_row only gets updated if not in Defect mode AND not entering
1216+
# Defect mode.
1217+
self.history_row = 1 if opponent.history[-1] == D else 0
1218+
if self.history[-1] == D:
1219+
self.history_row += 2
1220+
1221+
# If generous 2 turn ago and opponent defected last turn
1222+
if self.generous_n_turns_ago == 2 and opponent.history[-1] == D:
1223+
self.burned = True
1224+
1225+
if turn == 38 and opponent.history[-1] == D and opponent.cooperations == 36:
1226+
self.mode = "Fair-weather"
1227+
# These flags would already be set from turn == 37 logic below.
1228+
# Just take care to not lower this turn.
1229+
# self.more_coop = 2
1230+
# self.generous_n_turns_ago = 1 # 1 turn ago since this turn is ending.
1231+
return self.try_return(to_return=C, lower_flags=False)
1232+
1233+
1234+
if self.mode == "Fair-weather":
1235+
if opponent.history[-1] == D:
1236+
self.mode = "Normal" # Post-Defect is not possible
1237+
#Continue below
1238+
else:
1239+
# Never defect against a fair-weather opponent
1240+
return self.try_return(C)
1241+
1242+
# Continue with Normal mode
1243+
1244+
# Check for streaks
1245+
if self.detect_streak(opponent.history[-1]):
1246+
return self.try_return(D, inc_parity=True)
1247+
if self.detect_parity_streak(opponent.history[-1]):
1248+
self.parity_streak[self.parity_bit] = 0
1249+
self.parity_hits += 1
1250+
if self.parity_hits >= 8:
1251+
self.parity_limit = 3
1252+
return self.try_return(C, inc_parity=True) # Inc parity won't get used here.
1253+
1254+
if self.more_coop >= 1:
1255+
return self.try_return(C, inc_parity=True)
1256+
1257+
if turn < 37:
1258+
return self.try_return(opponent.history[-1], inc_parity=True)
1259+
elif turn == 37:
1260+
self.more_coop, self.generous_n_turns_ago = 2, 1
1261+
return self.try_return(D, lower_flags=False)
1262+
else:
1263+
if self.burned or random.random() > self.prob:
1264+
return self.try_return(opponent.history[-1], inc_parity=True)
1265+
else:
1266+
self.prob += 0.05
1267+
self.more_coop, self.generous_n_turns_ago = 2, 1
1268+
return self.try_return(D, lower_flags=False)

0 commit comments

Comments
 (0)