@@ -975,3 +975,333 @@ 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 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