/* Entrant: PredictorArrayBot George B. Moody (george@mit.edu) This mini-program maintains an array of predictors, tracks the recent performance of each predictor, and uses the best-performing predictor to intuit the opponent's next move. The predictors are simply the last 15 moves made by each player (and the increments of each of these). Hence on any given move, exactly one-third of the predictions will be winning moves, one-third will tie, and one-third will lose. The trick is to figure out which predictors (if any) are reliable. PredictorArrayBot uses the past performance of each predictor to assess its reliability. For example, against a non-exploitive opponent that makes (say) 7 random moves and then repeats those moves, PredictorArrayBot quickly identifies the opponent's seventh previous move as a good predictor of its next move. PredictorArrayBot's own moves are often effective predictors of an exploitive opponent's moves. I coded PredictorArrayBot in about half an hour, and spent a couple of hours tuning up its magic numbers (16, one of the factors of NP, which determines the depth of the search for a good predictor; and 24, the weighting constant in the ARMA filter, which determines how much extra emphasis is given the most recent moves). PredictorArrayBot's performance against the test suite is, sad to say, much too sensitive to the values of these numbers, so I was pleasantly surprised at its somewhat-better-than-middling performance in this year's competition. One of my research interests is identification and modelling of cardiac arrhythmias (abnormal heart rhythms). The inspiration for PredictorArrayBot comes from an idea for detecting atrial fibrillation, an arrhythmia that is characterized by seemingly unpredictable time intervals between heart beats. Interestingly, in a healthy person, the previous interval is usually not the best predictor of the next one, because respiration causes the healthy heart to speed up and slow down with each breath; thus if one breath lasts for (say) 6 heart beats, the sixth previous beat interval is likely to be a good predictor of the next one. In most abnormal rhythms, unusually short or (less often) long intervals repeat in predictable patterns. So the idea (first proposed by Paul Schluter in his 1980 MIT Ph.D. thesis) is to measure the absolute differences between the most recent interval and each of the previous N intervals (the prediction errors), and to select as the best predictor the one for which the prediction error is smallest on average; then if _none_ of the predictors has a small average error, the rhythm is probably atrial fibrillation. This detection algorithm does reasonably well, although better methods are known. */ #define NP (6*16) /* the number of predictors */ int predictorarraybot() { static double err[NP], smallest_err; static int choice, i, imax, mv, pred[NP], score; if ((mv = my_history[0]) == 0) for (i = score = 0; i < NP; i++) /* first move only */ err[i] = pred[i] = 0; /* Avoid using predictors that haven't been initialized. */ imax = (mv < NP/6) ? mv*6-1 : NP-1; /* Update the predictor array. */ for (i = imax; i >= 0; i--) /* Shift the older moves and their increments through the array, discarding the oldest ones. */ if (i > 5) pred[i] = pred[i-6]; /* Move in the opponent's latest move and its increments ... */ else if (i > 2) pred[i] = (opp_history[mv]+i)%3; /* ... and PredictorArrayBot's own move and increments. */ else pred[i] = (my_history[mv]+i)%3; /* Update the error array and find the best predictor. */ for (i = smallest_err = 6; i <= imax; i++) { err[i] += (((opp_history[mv] - pred[i] + 3) % 3 + 1) % 3 - err[i])/24.; /* No, that wasn't line noise! The expression ((opp_history[mv] - pred[i] + 3) % 3 + 1) % 3 (let's call it e0) evaluates to: 0 if pred[i] would have defeated the opponent's latest move, 1 if pred[i] would have tied, or 2 if pred[i] would have lost. So everything after the '+=' is a fraction of the difference between e0 and err[i]. What's happening above is thus: err[i] = (1/24)*e0 + (1 - 1/24)*err[i] In other words, err[i] is an autoregressive moving average (ARMA) filter applied to the sequence of errors (e0) for pred[i]. The magic number 24 is related to the characteristic time constant of the filter. */ if (err[i] < smallest_err) { smallest_err = err[i]; choice = pred[i-6]; /* Save the best move found so far. */ } } /* The next line keeps a running tally of how we're doing so far. */ score += ((((my_history[mv] - opp_history[mv]) + 3) % 3) + 1) % 3 - 1; /* The first two moves, and any moves made if we seem to be losing, are random; otherwise, we use the choice from the best predictor. */ return ((mv > 1 && score >= -trials/25) ? choice : (random() % 3)); } #undef NP