@@ -975,3 +975,294 @@ def strategy(self, opponent: Player) -> Action:
975
975
self .defect_padding = 0
976
976
977
977
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