1P5: Iterated Prisoner's Dilemma

35

13

This task is part of the First Periodic Premier Programming Puzzle Push and is intended as demonstration of the new challenge-type proposal.

The task is the write a program to play the iterated prisoner's dilemma better than other entrants.

Look, Vinny. We know your cellmate---what's his name? Yeah McWongski, the Nippo-Irish-Ukranian mobster--is up to something and you know what it is.

We're trying to be nice here, Vinnie. Givin' you a chance.

If you tells us what he's plannin' we'll see you get a good work assignment.

And if you don't...

The Rules of the Game

  • The contest consists of a full round-robin (all possible pairing) of two contestants at a time (including self plays).
  • There are 100 rounds played between each pair
  • In each round each player is asked to choose between cooperating with the other player or betraying them, without knowing the other players intentions in the matter, but with a memory of the outcomes of previous rounds played against this opponent.
  • Points are awarded for each round based on the combined choice. If both players cooperate they each get 2 points. Mutual betrayal yields 1 point each. In the mixed case, the betraying player is awarded 4 points and the cooperator is penalized by 1.
  • An "official" match will be run not sooner than 10 days after posting with all the submissions I can get to work and be used to select the "accepted" winner. I have a Mac OS 10.5 box, so POSIX solutions should work, but there are linuxisms that don't. Likewise, I have no support for the win32 API. I'm willing to make a basic effort to install things, but there is a limit. The limits of my system in no way represent the limits of acceptable responses, simply those that will be included in the "offical" match.

The Programmer's interface

  • Entries should be in the form of programs that can be run from the command line; the decision must the (sole!) output of the program on the standard output. The history of previous rounds with this opponent will be presented as a command-line argument.
  • Output is either "c" (for clam up) or "t" (for tell all).
  • The history is a single string of characters representing previous rounds with the most recent rounds coming earliest in the string. The characters are
    • "K" (for kept the faith meaning mutual cooperation)
    • "R" (for rat b@st@rd sold me out!)
    • "S" (for sucker! meaning you benefited from a betrayal)
    • "E" (for everyone is looking out for number one on mutual betrayal)

The bracket

Four players will be provided by the author

  • Angel -- always cooperates
  • Devil -- always talks
  • TitForTat -- Cooperates on the first round then always does as he was done by in the last round
  • Random -- 50/50

to which I will add all the entries that I can get to run.

The total score will be the sum score against all opponents (including self-plays only once and using the average score).

Entrants

(current as of 2 May 2011 7:00)

The Secret Handshake | Anti-T42T Missile | Mistrust (variant) | Anti-Handshake | The Little Lisper | Convergence | Shark | Probabimatic | Pavlov - Win Stay, Lose Switch | Honor Among Thieves | Help Vampire | Druid | Little Schemer | Bygones | Tit for Two Tats | Simpleton |

Scorer

#! /usr/bin/python
#
# Iterated prisoner's dilemma King of Hill Script Argument is a
# directory. We find all the executables therein, and run all possible
# binary combinations (including self-plays (which only count once!)).
#
# Author: dmckee (https://codegolf.stackexchange.com/users/78/dmckee)
#
import subprocess 
import os
import sys
import random
import py_compile

###
# config
PYTHON_PATH = '/usr/bin/python' #path to python executable

RESULTS = {"cc":(2,"K"), "ct":(-1,"R"), "tc":(4,"S"), "tt":(1,"E")}

def runOne(p,h):
    """Run process p with history h and return the standard output"""
    #print "Run '"+p+"' with history '"+h+"'."
    process = subprocess.Popen(p+" "+h,stdout=subprocess.PIPE,shell=True)
    return process.communicate()[0]

def scoreRound(r1,r2):
    return RESULTS.get(r1[0]+r2[0],0)

def runRound(p1,p2,h1,h2):
    """Run both processes, and score the results"""
    r1 = runOne(p1,h1)
    r2 = runOne(p2,h2)
    (s1, L1), (s2, L2) = scoreRound(r1,r2), scoreRound(r2,r1) 
    return (s1, L1+h1),  (s2, L2+h2)

def runGame(rounds,p1,p2):
    sa, sd = 0, 0
    ha, hd = '', ''
    for a in range(0,rounds):
        (na, ha), (nd, hd) = runRound(p1,p2,ha,hd)
        sa += na
        sd += nd
    return sa, sd


def processPlayers(players):
    for i,p in enumerate(players):
        base,ext = os.path.splitext(p)
        if ext == '.py':
            py_compile.compile(p)
            players[i] = '%s %sc' %( PYTHON_PATH, p)
    return players

print "Finding warriors in " + sys.argv[1]
players=[sys.argv[1]+exe for exe in os.listdir(sys.argv[1]) if os.access(sys.argv[1]+exe,os.X_OK)]
players=processPlayers(players)
num_iters = 1
if len(sys.argv) == 3:
    num_iters = int(sys.argv[2])
print "Running %s tournament iterations" % (num_iters)
total_scores={}
for p in players:
    total_scores[p] = 0
for i in range(1,num_iters+1):
    print "Tournament %s" % (i)
    scores={}
    for p in players:
        scores[p] = 0
    for i1 in range(0,len(players)):
        p1=players[i1];
        for i2 in range(i1,len(players)):
            p2=players[i2];
#        rounds = random.randint(50,200)
            rounds = 100
            #print "Running %s against %s (%s rounds)." %(p1,p2,rounds)
            s1,s2 = runGame(rounds,p1,p2)
            #print (s1, s2)
            if (p1 == p2):
                scores[p1] += (s1 + s2)/2
            else:
                scores[p1] += s1
                scores[p2] += s2

    players_sorted = sorted(scores,key=scores.get)
    for p in players_sorted:
        print (p, scores[p])
    winner = max(scores, key=scores.get)
    print "\tWinner is %s" %(winner)
    total_scores[p] += 1
print '-'*10
print "Final Results:"
players_sorted = sorted(total_scores,key=total_scores.get)
for p in players_sorted:
    print (p, total_scores[p])
winner = max(total_scores, key=total_scores.get)
print "Final Winner is " + winner
  • Complaints about my horrible python are welcome, as I am sure this sucks more than one way
  • Bug fixes welcome

Scorer Changelog:

  • Print sorted players and scores, and declare a winner (4/29, Casey)
  • Optionally run multiple tournaments (./score warriors/ num_tournaments)) default=1 , detect & compile python sources (4/29, Casey)
  • Fix particularly dumb bug in which the second player was being passed a incorrect history. (4/30, dmckee; thanks Josh)

Initial warriors

By way of example, and so that the results can be verified

Angel

#include <stdio.h>
int main(int argc, char**argv){
  printf("c\n");
  return 0;
}

or

#!/bin/sh
echo c

or

#!/usr/bin/python
print 'c'

Devil

#include <stdio.h>
int main(int argc, char**argv){
  printf("t\n");
  return 0;
}

Random

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
int main(int argc, char**argv){
  srandom(time(0)+getpid());
  printf("%c\n",(random()%2)?'c':'t');
  return 0;
}

Note that the scorer may re-invoke the warrior many times in one second, so a serious effort must be made to insure randomness of the results if time is being used to seed the PRNG.

TitForTat

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char**argv){
  char c='c';
  if (argv[1] && (
          (argv[1][0] == 'R') || (argv[1][0] == 'E')
          ) ) c='t';
  printf("%c\n",c);
  return 0;
}

The first one that actually does something with the history.

Running the scorer on only the provided warriors yields

Finding warriors in warriors/
Running warriors/angel against warriors/angel.
Running warriors/angel against warriors/devil.
Running warriors/angel against warriors/random.
Running warriors/angel against warriors/titfortat.
Running warriors/devil against warriors/devil.
Running warriors/devil against warriors/random.
Running warriors/devil against warriors/titfortat.
Running warriors/random against warriors/random.
Running warriors/random against warriors/titfortat.
Running warriors/titfortat against warriors/titfortat.
('warriors/angel', 365)
('warriors/devil', 832)
('warriors/random', 612)
('warriors/titfortat', 652)

That devil, he's a craft one, and nice guys apparently come in last.

Results

of the "official" run

('angel', 2068)
('helpvamp', 2295)
('pavlov', 2542)
('random', 2544)
('littleschemer', 2954)
('devil', 3356)
('simpleton', 3468)
('secrethandshake', 3488)
('antit42t', 3557)
('softmajo', 3747)
('titfor2tats', 3756)
('convergence', 3772)
('probabimatic', 3774)
('mistrust', 3788)
('hyperrationalwasp', 3828)
('bygones', 3831)
('honoramongthieves', 3851)
('titfortat', 3881)
('druid', 3921)
('littlelisper', 3984)
('shark', 4021)
('randomSucker', 4156)
('gradual', 4167)
        Winner is ./gradual

dmckee --- ex-moderator kitten

Posted 2011-04-29T21:07:00.847

Reputation: 2 726

2If my cellmate is Nippo-Irish-Ukrainian, why does his name look Hiberno-Sino-Russian? – Peter Taylor – 2011-04-29T22:32:56.443

Is it allowed to submit a strategy similar or identical to one of your "default warriors"? I.e., can I submit Tit-for-tat myself? (Cf. Rapaport in Axelrod's IPD tourney.) Or is this just boring? – jscs – 2011-04-29T22:45:51.517

2@Peter: LOL. The truth? Well, (1) the genealogies aren't clear, but I probably come by my mic'edness by way of the Scotch-Irish; (2) after I'd written "Nippo" I tried out various bits of the names of my friends from the land of the rising sun and didn't like the way they scanned, so I went ahead and used a Chinese surname that sounded good instead, and (3) I wouldn't know the difference if they took turns beating me with tire irons. Which seems likely under the circumstances. – dmckee --- ex-moderator kitten – 2011-04-29T22:51:57.503

@Josh: ::shrug:: I haven't thought about it. I included it in the defaults because it is a well known simple-but-strong player. You'll note that Joey has submitted a nearly as simple variant, and that suggests one way to go forward. – dmckee --- ex-moderator kitten – 2011-04-29T22:54:35.820

@dmckee What's the policy on multiple submissions? – Casey – 2011-04-29T23:05:03.397

@Casey: Make sure they are distinct, but go ahead. – dmckee --- ex-moderator kitten – 2011-04-29T23:06:43.957

Hey guys, I've been hacking a lot on the scoring script. In particular I've added python and lisp compilation to speed things up, as well as parallelized the matches according to your cpu count. I've added some of the changes to the score script in @dmckee's post, but I don't want to intrude to much as not everyone may want these changes. Here is a link to the latest version.

– Casey – 2011-04-30T03:40:27.703

Okay, I really did find a bug this time. I'm not sure what causes it, possibly something weird about tuple assignment. I was running all-C against all-D and displaying the history. All-D's history v. all-C was wrong; it always had 'S' in the most recent position, and 'R' for the rest. The fix against the version in question body as of this comment (wish I didn't have to do this in a comment): Rewrite the following lines: 35: return (s1, L1), (s2, L2) 41: (na, nha), (nd, nhd) = runRound(p1,p2,ha,hd) Add two lines after 43: ha = nha + ha and hd = nha + hd – jscs – 2011-04-30T07:19:09.460

Sorry, for "all-C", read "Angel" and for "all-D" read "Devil". Using the old terminology. – jscs – 2011-04-30T07:42:57.547

@Josh Hm, there is definitely something screwy there, but I don't think your changes fix it.. shouldn't you expect to see all 'S' for the devil when he is playing the angel? – Casey – 2011-04-30T18:24:48.013

@Casey: That's what I'm seeing here with my change. Should I make a suggested edit to the post to make it more clear? – jscs – 2011-04-30T19:06:18.810

1@Josh: Would it be simple to change return (s1, L1+h1), (s2, L2+h1) to return (s1, L1+h1), (s2, L2+h2) [Note L2+h2 instead of L2+h1 at the end]? //Cut-n-paste mistake or something equally idiotic. Sheesh! – dmckee --- ex-moderator kitten – 2011-05-01T00:01:21.330

@dmckee: I was hopping you would set a random number of rounds :) – Eelvex – 2011-05-01T01:46:45.740

@Eelvex: That's just the kind of input I could have used when I was tinkering with this in the Sand Box.

– dmckee --- ex-moderator kitten – 2011-05-01T02:19:01.840

@dmckee: I know but, unfortunately, wasn't available till now :( – Eelvex – 2011-05-01T02:44:39.630

@dmckee: That looks like the bug! Sheesh, I haven't gotten enough sleep lately or something. – jscs – 2011-05-01T03:03:13.457

2

I've spent some time on the test script, and I'm pleased to announce an update here. This update adds a simple shell to the test script, which allows the user to manually run this bot vs. that bot, run tournaments with restricted fields and some other cool stuff. Feel free to make suggestions! Oh. And I owe @josh for the bot-vs-bot idea. It's really just a fancier implementation of his "trainer" script.

– arrdem – 2011-05-01T04:22:07.837

I've just posted a half-dozen new warriors. If anyone's concerned that I am "flooding the field" or something like that, I'm perfectly okay with only my first two or three entries going in to the actual tournament. I'm just having fun coming up with ideas. – jscs – 2011-05-01T08:46:18.190

@rmckenzie, I was thinking of doing that, so thanks for doing it first. – Peter Taylor – 2011-05-01T11:25:29.597

@mckenzie, if I run your score.py with one argument I get a warning about NUM_ROUNDS being assigned to before global declaration and an NameError because num_games is not defined. – Peter Taylor – 2011-05-01T13:54:17.663

1@Josh: My only concern is that running the whole round-robin goes by O(N^2). I'm prepared to let the "official" math run for a while, but at some point it could get to be ridiculous. – dmckee --- ex-moderator kitten – 2011-05-01T13:56:47.153

@peter - thanks for pointing that out, I got it fixed. Updated on github. – arrdem – 2011-05-01T14:25:07.360

@rmckenzie Nice changes! Wanna make a pull request on github? – Casey – 2011-05-01T16:17:02.460

@mckenzie, I tried implementing the -v option of match by making a copy of runGame which adds print "\n", ha, "\n", hd after the line which prints the score. If I run angel vs devil for a 5-round match I get RRRRR for the angel (correct) and SRRRR for the devil (should be SSSSS). The last line of runRound has a bug. Should be return (s1, L1+h1), (s2, L2+h2) – Peter Taylor – 2011-05-01T16:46:33.593

1

@Peter It is fixed in the most recent commit

– Casey – 2011-05-01T17:03:35.727

@dmckee: I certainly understand that. At this point, I just walk away from the computer every time I run a tournament! – jscs – 2011-05-01T17:24:17.143

@dmckee: What was your reason for only counting the mirror match once? To reduce benefits of collusion? Not complaining, just curious. Every other match gets run twice. E.g., in a tourney of Angel and Devil, we have A v. D and D v. A. – jscs – 2011-05-01T17:28:44.580

@peter - Thanks. I just saw that... turns out my Lisper was doing so well in part because of that bug sigh but yeah, casey's repo and mine both have that fixed. @josh - I think that's a bug in the iteration generator. A v. D == D v. A, so there's no reason to run that round. Fixing that would halve the runtime.... – arrdem – 2011-05-01T17:39:04.013

@Josh, my scorer only runs one of A vs. D or D vs. A. Look at how the loops index. The reason for that was to keep the time down as rmckenzie says. If I was running both versions of the non-identical pairing then you are right: I would need to count both sides of the self-plays. – dmckee --- ex-moderator kitten – 2011-05-01T20:12:06.233

@dmckee: I must've knocked something out of whack when I made the ProvingGrounds script. My bug. Is your version still official? Are you planning to use either Casey or rmckenzie's changes? – jscs – 2011-05-01T20:27:29.520

@Josh some of Casey's changes have been applied to the version in the post here. I have not been watching the other versions very closely. I was going to grab the pre-compiling bits for the "official" version. Note that I keep putting "official" in quotes, 'cause I figure anyone can set a bounty, run their own version, and declare their own winner. Maybe a multi-round elimination tournament to support multiple matches and random numbers of rounds (since the "official" version is very much at he mercy of chance due to the randomness employed by some warriors and the short length). – dmckee --- ex-moderator kitten – 2011-05-01T20:32:14.743

That said, I'm sticking by the posted rules for the purposes of the green check-mark. – dmckee --- ex-moderator kitten – 2011-05-01T20:33:07.193

I'm voting for every entry as soon as I get it working on my machine. Gotta do Josh's new one soon. – dmckee --- ex-moderator kitten – 2011-05-01T20:33:39.033

Yet another update to the scoring code if you care... added some population manipulation controls. same github

– arrdem – 2011-05-02T00:38:50.750

I am seeing extremely incorrect round-by-round score results using the @Casey/@rmckenzie scorer. It's most noticeable using Angel or Devil and a bot that won't defect first. Examples: Devil v. Shark (200, 200), should not be a C-tie; Handshake v. Pavlov (200, 200), likewise; Angel v. Pavlov (103, 98), should be a C-tie; Angel v. Probabimatic (400, -100), just plain impossible. I would guess it's the threading screwing up just the printing, except that the final results of the tournaments are also off somewhat compared to what I'm seeing with the "official" scorer. Anyone else seeing this? – jscs – 2011-05-02T06:41:43.543

@Josh, I'm not seeing that precisely, but some results aren't what I'd expect. E.g. handshake beats anti-handshake 109-94 (although if either has been updated recently I'm out of date). – Peter Taylor – 2011-05-02T10:22:05.887

@josh - I thought I was seeing funny results but wrote it off as just me. thanks for the heads up. will investigate those cases as soon as I have time. – arrdem – 2011-05-02T10:42:36.347

Okay. for a 1v1 with debugging on, shark vs. devil shows this:devil EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEES shark EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEER. Looks fine. Angel-pavlov is also a c-tie as it should be. no unexpected results whatsoever. – arrdem – 2011-05-02T11:50:48.830

Okay. Last post in defense of the new scoring code, I swear. Running the same round-robin tests on the "official" code above and on the casey/rmckenzie scorer, the average difference in the performance of individual strategies is 18 points in 2300 point average scores. The population was constant between tests. – arrdem – 2011-05-03T20:11:16.337

I think there must be some system difference. The official results seem to agree with @rmckenzie, but I see about the same results with the official scoring code as with his scorer - Random Sucker wins, gradual comes second, then Wasp, Druid and Shark fight it out for third. Anyone got any ideas? The best that I can speculate is a difference in random number quality between python implementations. – Peter Taylor – 2011-05-15T22:09:01.897

@Peter: The full set includes several warriors that use randomness. To test for consistency of scorers we should test with the set of deterministic warriors. I make that to be: angle antiT4T bygones devil druid gradual helpvamp honor.. hyperrat.. littlelisper mistrust pavlov proba.. secret.. shark softmajo T42T T4T. I'll run mine now. – dmckee --- ex-moderator kitten – 2011-05-15T22:14:22.203

@dmckee, ok. I'll leave it running 5 tournament iterations overnight and post a summary of results tomorrow. – Peter Taylor – 2011-05-15T22:22:44.037

@dmckee - good point. I'll do my own test just for grins. – arrdem – 2011-05-16T01:28:29.377

In 5 runs, Wasp is always first, Gradual is always second, T42T and Mistrust are 3rd and 4th in some order, T4T is 5th. There is some slight fluctuation, so unless one of them is in fact partially random there's a bug somewhere. Edit: of course, handshake now generates a random secret. – Peter Taylor – 2011-05-16T06:08:37.717

1Peter: There are plenty of strategies in the field that don't behave deterministic. And they can easily affect the score of the top scorers. – Joey – 2011-05-22T18:13:32.470

I am relieved to see that Help Vampire came in second to last, only above Angel! Thanks for running this tournament, dmckee; it was fun! – jscs – 2011-06-01T22:04:50.523

@Josh: It was fun, and most of the credit goes to the many and enthusiastic participants. – dmckee --- ex-moderator kitten – 2011-06-01T22:43:06.963

2Interesting: There were 23 contestents, so each played 22 rounds. If everyone had played "Angel" every score would have been 4400, but even the best score of 4167 did not match that. If only we lived in a perfect world... :) – Briguy37 – 2011-09-09T18:19:09.880

Answers

11

Gradual

This strategy is based on a paper by Beaufils, Delahaye and Mathieu. My C really isn't the best, so if anyone has any suggestions to improve/speed up the code, let me know!

[Edit] Worth to note is that Gradual was designed to be a strategy that outperforms Tit for Tat. It has similar properties in that it is willing to cooperate and retaliates against a defecting opponent. Unlike Tit for Tat, which only has a memory of the last round played, Gradual will remember the complete interaction and defect the number of times the opponent has defected so far. It will offer mutual cooperation afterwards again, though.

As usual, the performance of the strategy depends a bit on the line-up of other strategies. Also the original paper wasn't really clear on some details.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char* argv[]) {
    if(argc == 1){
        printf("c\n");
        return 0;
    }

    size_t l = strlen(argv[1]);
    int i;
    size_t currentSequence = 0;
    size_t totalDefects = 0;
    size_t lastDefects = 0;

    for(i = l-1; i >= 0; i--){
        if(argv[1][i] == 'E' || argv[1][i] == 'R'){
            totalDefects++;
            currentSequence = 0;
        } else if(argv[1][i] == 'S') {
            currentSequence++;
        }
    }

    if(currentSequence < totalDefects)
        // continue defect sequence
        printf("t\n");
    else if(argv[1][0] == 'S' || argv[1][0] == 'E' ||
            argv[1][1] == 'S' || argv[1][1] == 'E')
        // blind cooperation
        printf("c\n");
    else if(argv[1][0] == 'R')
        // start new defect sequence
        printf("t\n");
    else
        printf("c\n");

    return 0;
}

Ventero

Posted 2011-04-29T21:07:00.847

Reputation: 9 842

11

The Secret Handshake

#!/usr/bin/python
import sys
import random

def main():
    if len(sys.argv) == 1:
        hist = ""
    else:
        hist = sys.argv[1]
    if len(hist) <= len(TAG) and hist == TAGMATCH[len(TAG) - len(hist):]:
        print TAG[len(TAG) - len(hist) - 1]
        return
    if hist[-len(TAG):] == TAGMATCH:
        print 'c'
        return
    print "t"

def getTag():
    global TAG
    filename = sys.argv[0]
    filename = filename.replace(".pyc", ".py")
    f = open(filename, 'r')
    code = f.read().split('\n')
    f.close()
    if len(code[1]) == 0 or code[1][0] != '#':
        random.seed()
        newtag = 't' * 10
        cs = 0
        while cs < 3:
            pos = random.randint(0, 8)
            if newtag[pos] == 't':
                newtag = newtag[:pos] + 'c' + newtag[pos+1:]
                cs += 1
        code.insert(1, '#%s' % newtag)
        f = open(filename, 'w')
        f.write('\n'.join(code))
        f.close()
        TAG = newtag
    else:
        TAG = code[1][1:]
    global TAGMATCH
    TAGMATCH = TAG.replace('c', 'K').replace('t', 'E')

if __name__ == "__main__":
    getTag()
    main()

The strategy here is to sacrifice the first 10 rounds to performing a "secret" handshake. If I'm celled with myself, I then recognize the history of the first 10 moves and put on my Angel cap for the rest of the game. As soon as I recognize that my cellmate isn't myself, I transform into a Devil in an attempt to take advantage of overly cooperative cellmates.

Whether sacrificing the first 10 rounds will allow me to edge out the Devil itself depends strongly on how many entries there are. To minimize the damage, only 3 cooperates show up in the handshake.

Edit: TAGMATCH dynamic now to prevent stupid errors like changing only one of them and so I can make TAG dynamic at some point in the future.

Edit 2: Now randomly generates the tag on the first run and stores it in the file specified by sys.argv[0] (.pyc replaced by .py so it goes to the code, not bytecode, file). I think this is the only information all of my instances have that no one else has, so it seems like the only option for avoiding parasites.

Aaron Dufour

Posted 2011-04-29T21:07:00.847

Reputation: 219

But how does your doppelganger know to make itself a devil? – arrdem – 2011-04-30T03:44:57.657

1(I feel like a parrot, saying "Tit for Tat" all the time...) Note that T4T will beat your strategy in a pairing against: T4T (cooperates earlier) and Devil (fewer Rat results), and will tie with your strategy. Of course, the grand total, not the pairing total, is what counts in the end. As you say, the population is important. – jscs – 2011-04-30T04:16:07.100

1Oh, no, you get one extra S out of Tit for Tat. Nice. I didn't realize TAG was being played backwards. However, shouldn't your TAGMATCH be 'KEEEKEEEKE'? "".join({('c', 'c'):'K', ('t', 't'): 'E'}[moves] for moves in zip(TAG, TAG)) – jscs – 2011-04-30T05:25:23.757

@John Good point - I originally had a different TAG and when I changed it to minimize cooperation, I forgot to update TAGMATCH. @Arrdem The idea is that if I'm playing against myself, the best thing to do is for both to cooperate all the time to maximize the sum of their scores. – Aaron Dufour – 2011-04-30T18:28:56.097

New version should prevent parasites. In order to hold on to the efficiencies I built in to the original tag, the final character (first move) must be a 't' (can't remember why this mattered...) and exactly 3 'c's will occur in the sequence. – Aaron Dufour – 2011-05-02T05:28:47.867

Nicely done! (You forgot import random though.) – jscs – 2011-05-02T07:03:26.123

1Aww, damn it. So now I have to search all the .py files for your code and extract the TAG. I won't do that in C, though ... – Joey – 2011-05-02T08:18:57.837

@Josh Good catch. I had it in my code, but I only edited in the diffs and apparently missed that one. @Joey I must say, I didn't see that coming. Hmmmm... – Aaron Dufour – 2011-05-02T18:20:10.930

@Aaron: Joey's move is the clear next step. This is fun to watch! – jscs – 2011-05-02T18:23:47.340

Aaron: Don't fear, I won't. The stateless nature of execution makes writing code a little painful and I'm not really good in thinking about actual strategies :-) – Joey – 2011-05-03T12:28:43.513

@Joey: Don't fear either. If you're not going to do it, expect "The Handshake Virus" in Python to appear later in the week! :p (You still get original credit, of course!) – jscs – 2011-05-03T19:08:42.590

6

The Little Lisper

(setf *margin* (/ (+ 40 (random 11)) 100))
(setf *r* 0.0)
(setf *s* 0.0)
(setf *k* 0.0)
(setf *e* 0.0)

;; step 1 - cout up all the games results

(loop for i from 1 to (length(car *args*)) do
    (setf foo (char (car *args*) (1- i)))
    (cond 
        ((equal foo #\R) (setf *r* (1+ *r*)))
        ((equal foo #\S) (setf *s* (1+ *s*)))
        ((equal foo #\K) (setf *k* (1+ *k*)))
        ((equal foo #\E) (setf *e* (1+ *e*)))
    )
)

(setf *sum* (+ *r* *s* *k* *e*))

;; step 2 - rate trustworthiness
(if (> *sum* 0)
    (progn
        (setf *dbag* (/ (+ *r* *e*) *sum*)) ; percentage chance he rats
        (setf *trust* (/ (+ *s* *k*) *sum*)); percentage chance he clams
    )
    (progn
        (setf *dbag* 0) ; percentage chance he rats
        (setf *trust* 0); percentage chance he clams
    )
)



;; step 3 - make a decision (the hard part....)

(write-char
    (cond
        ((> *sum* 3) (cond 
                    ((or (= *dbag* 1) (= *trust* 1)) #\t) ; maximizes both cases
                                                          ; takes advantage of the angel, crockblocks the devil
                    ((> (+ *dbag* *margin*) *trust*) #\t) ; crockblock statistical jerks
                    ((< *dbag* *trust*) #\c)              ; reward the trusting (WARN - BACKSTABBING WOULD IMPROVE SCORE)
                    ((and
                        (= (floor *dbag* *margin*) (floor *trust* *margin*))
                        (not (= 0 *dbag* *trust*)))
                        #\t)                              ; try to backstab a purely random opponent, avoid opening w/ a backstab
                    )
        )
        (t #\c)                                            ; defalt case - altruism
    )
)

The Devil

Consider the following, format (Player1, Player2)

  • (C, T) - P2 gains FOUR POINTS for his treachery, while P1 LOOSES ONE
  • (T, T) - P2 AND P1 GAIN 1

Assuming that P2 is the devil, there is no way that the devil can ever loose points, in fact the worst that he can do is gain only one point. Up against a purely random opponent therefore, the devil's worst possible score will be exactly (5/2)*n where n is the number of "games" played. His absolute worst-case is against himself, where his score will be n, and his best-case is against an angel, which will be 4*n

Assert : optimal_strat = devil

this is a tourney. Backstabbing my cell-mate is a much better strategy than cooperation because it helps MY SCORE more (+4). BONUS - he gets slammed (-1)! If I stick my neck out for him, I stand to gain (+2) and loose (-1). Therefor statistically backstabbing is rewarded.

But Is It Optimal?

There is no reason to EVER (under this scoring system) co-operate.

  • If you chose the wrong time to clam up, you loose out.
  • If you rat, at least you don't loose anything.
  • If you rat and he's dumb, you gain 2x more than if you had been a good pall.

In the KOTH system, maximization of returns is essential. Even if you have two bots who get perfectly synced and co-operate, their individuals scores will still only get boosted by 200 points for their sportsmanship. A devil on the other hand will earn at least 100 points, with an average case of 200 and a maximum of 400, and cost his opponents up to 100 points each! So practically, the devil really scores an average game of 300, spiking to 500.

Bottom line - time will tell

To me, it looks like the scoring should be re-considered lest the devil take the day. Increasing the co-operation score to 3 all might do it. It is possible however to detect devils and prevent them from scoring their full 400, as pavlov and spite show. Can I prove that either one will pick up enough points for their cooperation to justify their faith? no. All of that is dependent on the final field of contenders.

GL, HF!

And please do your worst to this post. I wanna write my senior paper on this when all's said and done.

Version history

  1. Added a margin variable which changes Lisper's tolerance for duchebaggery randomly.
  2. Updated lisper to clam for the first two rounds to get off on the right foot with co-operative opponents
  3. Used a genetic algorithm to find the most robust values for the random threshold generator based on their maximum cumulative score against a standard set of opponents. Posted update including them.

OFFICIAL VERSION OF LISPER

DEVEL VERSION OF LISPER

arrdem

Posted 2011-04-29T21:07:00.847

Reputation: 805

The scoring varies in different variants of the game. I did play around with increasing the cooperation incentive, and I agree that it will have an effect on the strategies chosen. The good news: you can grab the scorer, set your own rules and try it. In principle you could even offer a bounty. – dmckee --- ex-moderator kitten – 2011-04-30T02:16:15.927

fink install clisp ::tapping fingers repeatedly:: – dmckee --- ex-moderator kitten – 2011-04-30T02:19:18.140

BTW-- I think that that optimal strategy actual depends on the behavior of the other competitors. Enough warriors that default to good, but detect bad faith can mean that devil averages little more than 1 point per round, while the goody-twoshoes brigade averages closer to 2 points per round. But the balance is presumably delicate, and it doesn't take many bad apples to spoil the fun. – dmckee --- ex-moderator kitten – 2011-04-30T02:22:59.587

@dmckee: optimal strategy is greatly affected by the population. You've seen IPD's where population changes round to round? Tit for Tat can never do better on its own than Devil, but Tit for Tat plus another strategy that will cooperate always do better than two Devils. – jscs – 2011-04-30T02:29:58.773

@josh, d - I can't wait to see the final results... I suspect that "statistical devils" which randomly rat with some P < .25 could do quite well, beating out some of the above "trust" plans, depending on the devil detector standards. T42T and T4T are very vulnerable to this. Bygones should loose to this too. – arrdem – 2011-04-30T02:55:55.800

@dmckee - you are invoking this as $ ./foo.lsp (str) where str is something like krsskskeees without quotes, ya? – arrdem – 2011-04-30T02:58:53.360

@Arrdem: Ah! the history is in capitals, and on the first round no argument is passed (because of the line Popen(p+" "+h,stdout=subprocess.PIPE,shell=True) with h = ''). The second one may be a defeatable, but I haven't figured it out yet. – dmckee --- ex-moderator kitten – 2011-04-30T03:03:00.143

@Arrdem: I fully expect to see Bygones somewhere in the lower-middle of the pack; I just want the data! :) Have you read Hofstadter's discussion of Axelrod's two IPD tourneys? It's funny, I was just looking at it last week. There's a special mention of JOSS, which rats with a small random probability -- it lost to T4T. T4T won both of Axelrod's tourneys, in fact. Not that it always will, of course; T42T apparently would have won the first had it been entered (according to "subjunctive replay"), but was actually 24th in the second. The score matrix is different here, for one of many things. – jscs – 2011-04-30T03:13:58.657

@josh - nope. high school senior hear, all I've read is the mythical man-month and a sh*t load of programming/unix books. Thanks for the pointer! I'm surprised that the T4T series did so well, but I guess we'll see. – arrdem – 2011-04-30T03:20:30.790

The essay is: "The Prisoner's Dilemma Computer Tournaments and the Evolution of Cooperation" by Douglas Hofstadter (cool guy!); it was originally from his Scientific American column "Metamagical Themas" and is reprinted in his book of the same name. The whole book is a good read. BTW, nice analysis of the tournament; I didn't mention that before. – jscs – 2011-04-30T03:52:52.897

I've got the tourney loaded up on my linux box and I'm running all the contenders.... I'm interested to see that with one instance of each contender, T4T is currently coming in fourth behind (from 3rd up) lisper, handshake (WTF?) and convergence. your bygones is actually 5th. – arrdem – 2011-04-30T03:57:05.317

I've been running them as they come in. Convergence is not always that good. There is a surprising amount of variation from round to round (from the players the include randomness). If you're interested you can use the multiple tournaments feature @casey provided to examine it. Very interesting. – dmckee --- ex-moderator kitten – 2011-04-30T04:03:11.473

@dmckee yea, I am experiencing lots of variation too. It is interesting to see how the results are changing as the population changes. Before lisper and handshake, Pavlov seemed to do pretty well, but now he's down in 7th. What gets me is random seems to be doing extraordinarily well! – Casey – 2011-04-30T04:18:49.963

@dmckee: One interesting thing to try if you have the inclination is changing the population; e.g., three Devils, half a dozen Random, and one of everything else. It could be hard to vary that in such a way as to be able to extract useful information, however. – jscs – 2011-04-30T04:56:51.633

@Casey: @dmckee: Have you noticed that Handshake has a bug? It doesn't recognize itself. I left a comment about it. – jscs – 2011-04-30T06:49:24.653

@josh yeah. I saw that too...... fail. – arrdem – 2011-04-30T06:53:40.057

Well... there's no reason it can't be fixed. All it takes is changing the TAGMATCH. I've got it running fixed here, and it works. – jscs – 2011-04-30T07:09:38.950

@josh - but how well? I just made some changes to lisper, and it's now taking 1st consistently. will post changes. – arrdem – 2011-04-30T07:18:01.417

Yes, this is quite a robust strategy. Well done! – jscs – 2011-04-30T17:50:11.420

@josh - thanks! I tried a bunch of more "evil" variants which featured random backstabbing, and I was interested to find that this strategy which is really just a more intelligent T4T does better by playing nice... Maybe Axelrod had something right after all. – arrdem – 2011-04-30T18:35:24.453

If you haven't looked at the Wikipedia page for the Prisoner's Dilemma, you should -- it describes Axelrod's analysis of characteristics of a successful strategy: Nice, Retaliating, Forgiving, Non-envious.

– jscs – 2011-04-30T18:53:44.633

1@josh - thanks for the link. I read some other wikipedia pages on this dilemma, but I missed that section. A rules bug I just noticed, there are no rules against entries using the filesystem. this creates the potential for much more efficient co-operation along the lines of the handshake. – arrdem – 2011-04-30T19:26:52.750

True, but since the code's open, parasites can use that knowledge too. I'll be interested to see what Aaron comes up with as a countermeasure. I haven't had any good ideas along those lines yet. – jscs – 2011-04-30T19:36:30.963

3There is no reason to EVER (under this scoring system) co-operate is only half-correct. If you know that your opponent doesn't take the history into account (angel, devil, random) then you should always defect. If your opponent does take the history into account and you can sync then you can do better. I have a couple of ideas which revolve around detecting whether the opponent is rational or superrational. – Peter Taylor – 2011-05-01T11:29:43.693

1Aren't you getting divide-by-zero errors 3/20ths of the time with the latest version? Whenever (random 20) gives 2, 5, or 8, (/ (+1 rand-num) 10) is 0.3, 0.6, 0.9, and the remainder of division with 0.3 is 0; so (floor *dbag* *margin*) dies. – jscs – 2011-05-01T23:38:16.793

Actually looks like almost 1/5 of the time? – jscs – 2011-05-01T23:43:14.170

@josh - I hadn't noticed that.... well it's the price I pay for not being that rigorous when I threw the margin bit together. I'll patch it in a minute. Oh. I've got a new version of the scoring code out, it adds some cool stuff like running tournaments with custom population contents. Also fixes the AvD DvA bug. – arrdem – 2011-05-02T00:35:30.097

@rmckenzie: I've had quite a few tournaments crash on me because of it. I changed it to set *margin* to (max 0.09 calculated-value) as a stopgap. I'll incorporate your official fix now. I look forward to your upgraded scoring script! You should know, though, that the mistake was on my end regarding the AvD DvA. I had munged the code when I created my ProvingGrounds version and didn't realize it. dmckee's original code correctly runs only one match for each pairing. – jscs – 2011-05-02T02:04:24.130

I saw your meta-programming efforts. How did it go, and which is the new official version? – jscs – 2011-05-03T01:34:15.650

say what? Oh. The genetic algorithm crap on the repo? I wrote generators that varied the two constants which control the random number generator, then iteratively graded each variant based on its performance against the rest of the field. Right now, there isn't an official version. I will be posting an updated lisper shortly which will include the optimal constants shortly. If that's not the meta work you are referring to, please clarify. – arrdem – 2011-05-03T01:51:06.423

Seems to have got worse. I'm now seeing it around the 60th percentile. – Peter Taylor – 2011-05-03T06:30:06.643

Okay. then we have a problem... with my scorer, Lisper is in the lead fairly comfortably... debugging time </sigh> – arrdem – 2011-05-03T10:40:22.870

And there was much confusion.... my scorer is within 1% or less of the official scorer on all champions. At that level, the randomness in some of the champs is to blame, I can't imagine a programmatic failure which would cause such little deviation. You sure your software is good/up-to-date? (no offense....) – arrdem – 2011-05-03T21:36:52.327

@rmckenzie, I think it was the latest version of your scorer. Although I'm running your lisper with the old #!/usr/bin/env clisp shebang and a .lisp extension because it wasn't working for me with the .lsp extension and precompilation. – Peter Taylor – 2011-05-04T12:04:50.127

@rmckenzie, I've downloaded the absolute latest version of your scorer and run 5 tournaments with the full 24-warrior field. Lisper scores an average of 157.5 points per match, which puts it 13th (the addition of random_sucker has shaken things up a bit). The top 4 average over 170 points per match. Is this not what you observe? – Peter Taylor – 2011-05-04T17:57:58.807

Okay. Step 1 - reverted to dabb8aca. Step 2 - empty warriors and re-add all champions by copying from this page. Step 3 - run the score code from dabb8aca as "./score.py 5 100". Step 4 - use grep -e "^lisper [0-9]\{4\}" to isolate final scores. Step 5 - calculate the average of the values spat out by grep, avg of 4022 pts/iteration, or 168 pts/match. Will analyze other bots next. – arrdem – 2011-05-05T00:03:25.077

FROM THE SAME RUNS AS ABOVE - the winner was random-sucker averaging 170.7 pts/game. The worst was Angel at 83.7 pts/game. Population average: 144.7 pts/game, std. dev of 25.82 pts/game. That puts Lisper in the 86th percentile, or 4th place. dear lord.... what's going on here? – arrdem – 2011-05-05T00:47:01.543

@rmckenzie, only just saw your reply. The current version of score.py is broken (NameError at line 75 because global name 'scores' is not defined). I'm leaving a 5x100 tourney running with an old version of score.py and when it done I'll zip up all the files plus the log of the tournaments and give you a link. – Peter Taylor – 2011-05-07T22:36:08.580

Thanks peter. I confess your bad-results bug is driving me nuts because I can't replicate it. I'll have the global bug fixed asap, thanks for the heads-up. – arrdem – 2011-05-08T00:39:11.937

@peter - could you please include a) the scoring code you used FOR THE LOGGED 5x100, b) the version OF MY SCORING CODE used for the other results. I just want as much context as I can get in whacking this bug. Thanks! – arrdem – 2011-05-08T02:31:50.527

@rmckenzie, the zip is at http://www.cheddarmonk.org/prisoners.zip . I used the same version of your scoring code as used for other results - I'll grab the latest now and run it, just in case it changes things. I've also included a copy of /proc/cpuinfo in case that helps. I wonder whether it might be a multithreading issue, but I don't know anything about thread safety in Python. FWIW I've run all my tests on a quad-core machine.

– Peter Taylor – 2011-05-08T06:11:58.923

@peter - thanks for the zip. you'll love this: when I run the score.py file included in the zip unmodified I see lisper coming in fourth every match. As the only difference between the code's execution is that it runs four threads on your box and two on mine (one thread per cpu), I'm inclined to call it a parallel processing bug. @casey - sorry for the necro, but you got any ideas here? – arrdem – 2011-05-08T13:27:16.123

@rmckenzie, I changed .Pool(None) to .Pool(1), which I presume means that it single-threads it. First of 5 tournaments just finished with lisper 13th... – Peter Taylor – 2011-05-08T21:56:10.220

@peter - great. just great. I did some test too... racked threads all the way up to 16 and was unable to replicate your lisper score. Ignoring lisper, how do the other champions do are their scores constant between versions? If so, then the bug is somewhere in lisper or a difference between clisp on our boxes, not a bug in the scoring code. – arrdem – 2011-05-08T21:57:33.747

@rmckenzie, have you tried racking the threads down to 1? I don't see a significant difference between the quad-thread and the single-thread results. (Image to follow in a few hours) – Peter Taylor – 2011-05-09T12:38:49.683

@peter - I just tried that too, and I can confirm on that count. Idea. I'm running some rounds and will post a zip file with my code, bots and output once I'm done. What I would like you to try and do is to verify that on your box as well as mine, my scoring code squares with the "official" code. I will have a friend of mine do the same test. If you both have good results, then the bug is in clisp. I will also be re-implimenting lisper in python to see how that changes the game (it shouldn't). – arrdem – 2011-05-10T21:36:16.887

The reasoning that the devil might be best is as incorrect in this contest as it is in real life. It assumes all rounds are independent, but since there are 100 rounds in a row they aren't. And non-devils will gain more points in their competitions when both mostly cooperate. – Hans-Peter Störr – 2011-10-26T11:01:47.640

5

Mistrust (variant)

This one came out first in my own tests years ago (back then I was in 11th grade and did a tiny thesis on exactly this, using strategies devised by other students as well). It starts out with the sequence tcc ( and plays like Tit for Tat after that.

Apologies for the horrible code; if someone can make that shorter while not exactly golfing it, I'd be grateful :-)

#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) {
    if (argc == 1)
        printf("t\n");
    else switch (strlen(argv[1])) {
        case 0:
            printf("t\n");
            break;
        case 1:
        case 2:
            printf("c\n");
            break;
        default:
            if (argv[1][0] == 'R' || argv[1][0] == 'E')
                printf("t\n");
            else
                printf("c\n");
            break;
    }

    return 0;
}

Joey

Posted 2011-04-29T21:07:00.847

Reputation: 12 260

No need for duplicate code on length 1 and 2. Use fall through: case 1: case2: printf(...); break;. And gcc wants an explicit declaration of string.h to use strlen. In any case I have it running. – dmckee --- ex-moderator kitten – 2011-04-29T21:36:01.877

Ah, true that. I wasn't sure how to detect the very first round, though, whether there is an empty first argument (history) or just none. – Joey – 2011-04-29T21:41:48.040

I'm not sure. It's whatever python does with Popen(p+" "+h,stdout=subprocess.PIPE,shell=True) when h = ''. I'm guessing argc=1. – dmckee --- ex-moderator kitten – 2011-04-29T21:46:00.860

I think I'll play it safe and leave that like it's right now ;-) – Joey – 2011-04-29T21:55:42.177

1That initial sequence is quite a good idea, aiming squarely at Tit for Tat's weakness. You get a tiny lead on it, then play its way thereafter. – jscs – 2011-04-30T02:32:28.883

1@Josh, where's the tiny lead? Against T4T this starts out SRK and then continues with K. But SR is worth 3 points to each player. – Peter Taylor – 2011-04-30T10:24:10.623

@Peter: Yes, you're absolutely right. I wonder what I was thinking of? – jscs – 2011-04-30T16:49:24.820

5

Anti-T42T Missile

#!/usr/bin/python

"""
Anti-T42T Missile, by Josh Caswell

That Tit-for-two-tats, what a push-over!
  T42T: ccctcctcc...
AT42TM: cttcttctt...
        KSSRSSRSS...
"""
import sys
try:
    history = sys.argv[1]
except IndexError:
    print 'c'
    sys.exit(0)

if history[:2] == 'SS':
    print 'c'
else:
    print 't'

Does reasonably well against the base set of warriors: kills Angel, slightly beaten by Devil (but keeps his score low), generally beats RAND handily, and just barely beats Tit for Tat. Does poorly when playing against itself.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

I submitted an edit that makes this one actually work :) It needs to be approved. – Casey – 2011-04-30T18:40:09.387

@Casey: good lord, I'm making so many stupid mistakes in my enthusiasm for this problem! Thanks, but why did you eliminate the sh-bang? – jscs – 2011-04-30T19:02:11.213

Er, that was an accident. I'll add it back. – Casey – 2011-04-30T19:34:34.740

@Casey: no problem. I'll do it. Need to add a doc string anyways. – jscs – 2011-04-30T19:35:28.063

4

Convergence

Initially nice, then plays randomly with an eye on the opponent's history.

/* convergence
 *
 * A iterated prisoners dilemma warrior for
 *
 * Strategy is to randomly chose an action based on the opponent's
 * history, weighting recent rounds most heavily. Important fixed
 * point, we should never be the first to betray.
 */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char**argv){
  srandom(time(0)+getpid()); /* seed the PRNG */
  unsigned long m=(1LL<<31)-1,q,p=m;
  if (argc>1) {
    size_t i,l=strlen(argv[1]);
    for (i=l; --i<l; ){
      switch (argv[1][i]) {
      case 'R':
      case 'E':
    q = 0;
    break;
      case 'K':
      case 'S':
    q = m/3;
    break;
      }
      p/=3;
      p=2*p+q;
    }
  }
  /* printf("Probability of '%s' is %g.\n",argv[1],(double)p/(double)m); */
  printf("%c\n",(random()>p)?'t':'c'); 
  return 0;
}

I've tried diddling the weighting on the history, but haven't properly optimized it.

dmckee --- ex-moderator kitten

Posted 2011-04-29T21:07:00.847

Reputation: 2 726

4

Shark

#!/usr/bin/env python

"""
Shark, by Josh Caswell

Carpe stultores.
"""

import sys

HUNGER = 12

try:
    history = sys.argv[1]
except IndexError:
    print 'c'
    sys.exit(0)

if history.count('S') > HUNGER:
    print 't'
else:
    print 'c' if history[0] in "SK" else 't'

Does quite well against the base roster.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

... seize the clam? – arrdem – 2011-05-01T14:59:43.350

:) Seize the fools. – jscs – 2011-05-01T17:30:29.243

+1 for holding a consistent 2nd place in the current field. – arrdem – 2011-05-01T17:40:22.577

3

Pavlov - Win Stay, Lose Switch

On the first turn it cooperates, and then it cooperates if and only if both players opted for the same choice in the previous move.

#!/usr/bin/python
import sys

if len(sys.argv) == 1:
    print 'c'
else:
    hist = sys.argv[1]
    if hist[0] == 'K' or hist[0] == 'E':
        print 'c'
    else:
        print 't'

Casey

Posted 2011-04-29T21:07:00.847

Reputation: 3 129

Shouldn't this use hist[0] (hist[-1] is always the first move of the round)? – jscs – 2011-04-30T17:07:06.293

Oh wow, you're right. I assumed that the input string had the most recent rounds at the end of the string, not the beginning. Fixed. – Casey – 2011-04-30T17:13:33.737

3

"Probabimatic"

Starts by cooperating, then picks whichever option gives it the highest expected value. Simple.

#include <stdio.h>

void counts(char* str, int* k, int* r, int* s, int* e) {
    *k = *r = *s = *e = 0;
    char c;
    for (c = *str; c = *str; str++) {
        switch (c) {
            case 'K': (*k)++; break;
            case 'R': (*r)++; break;
            case 'S': (*s)++; break;
            case 'E': (*e)++; break;
        }
    }
}

// Calculates the expected value of cooperating and defecting in this round. If we haven't cooperated/defected yet, a 50% chance of the opponent defecting is assumed.
void expval(int k, int r, int s, int e, float* coop, float* def) {
    if (!k && !r) {
        *coop = .5;
    } else {
        *coop = 2 * (float)k / (k + r) - (float)r / (k + r);
    }
    if (!s && !e) {
        *def = 2.5;
    } else {
        *def = 4 * (float)s / (s + e) + (float)e / (s + e);
    }
}

int main(int argc, char** argv) {
    if (argc == 1) {
        // Always start out nice.
        putchar('c');
    } else {
        int k, r, s, e;
        counts(argv[1], &k, &r, &s, &e);
        float coop, def;
        expval(k, r, s, e, &coop, &def);
        if (coop > def) {
            putchar('c');
        } else {
            // If the expected values are the same, we can do whatever we want.
            putchar('t');
        }
    }
    return 0;
}

Used to start by cooperating, but now it seems that defecting actually works better. EDIT: Oh wait, it actually doesn't.

Lowjacker

Posted 2011-04-29T21:07:00.847

Reputation: 4 466

1

Another statistician! Let's see how this plays against its fellow calculators!

– jscs – 2011-04-30T21:16:48.147

By the way, if you change for (char c = *str; to char c; for (c = *str; then gcc will compile this without complaining that it needs to be put into C99 mode. – Peter Taylor – 2011-05-02T20:12:13.180

3

Honor Among Thieves

#!/usr/bin/env python

"""
Honor Among Thieves, by Josh Caswell

I'd never sell out a fellow thief, but I'll fleece a plump mark,
and I'll cut your throat if you try to cross me.
"""

from __future__ import division
import sys

PLUMPNESS_FACTOR = .33
WARINESS = 10

THIEVES_CANT = "E" + ("K" * WARINESS)

try:
    history = sys.argv[1]
except IndexError:
    history = ""

if history:
    sucker_ratio = (history.count('K') + history.count('S')) / len(history)
    seem_to_have_a_sucker = sucker_ratio > PLUMPNESS_FACTOR


# "Hey, nice t' meetcha."
if len(history) < WARINESS:
    #"Nice day, right?"
    if not set(history).intersection("RE"):
        print 'c'
    # "You sunnuvab..."
    else:
        print 't'

# "Hey, lemme show ya this game. Watch the queen..."
elif len(history) == WARINESS and seem_to_have_a_sucker:
    print 't'

# "Oh, s#!t, McWongski, I swear I din't know dat were you."
elif history[-len(THIEVES_CANT):] == THIEVES_CANT:

    # "Nobody does dat t' me!"
    if set(history[:-len(THIEVES_CANT)]).intersection("RE"):
        print 't'
    # "Hey, McWongski, I got dis job we could do..."
    else:
        print 'c'

# "Do you know who I am?!"
elif set(history).intersection("RE"):
    print 't'

# "Ah, ya almos' had da queen dat time. One more try, free, hey? G'head!"
elif seem_to_have_a_sucker:
    print 't'

# "Boy, you don't say much, do ya?"
else:
    print 'c'

Note that the THIEVES_CANT is essentially a handshake, though it will only emerge when playing against a cooperator. However, it avoids the parasite problem by checking for later crosses. Does quite well against the base roster.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

+1 for being the first strat to reliably trounce lisper. Average margin of victory - 300 pts. – arrdem – 2011-05-01T15:02:38.397

Seems to be the strongest in a tourney run of the current field. – Peter Taylor – 2011-05-01T16:23:14.090

Actually, no, Druid is now that I've fixed the bug in the scorer. – Peter Taylor – 2011-05-01T16:56:32.723

@rmckenzie, @Peter: Geez, really? I was just going for personality. – jscs – 2011-05-01T17:17:32.933

@josh - not any more.... on the new scoring code @casey's scoring code Lisper is back on top followed by shark. – arrdem – 2011-05-01T17:32:07.017

@rmckenzie: Druid is (quite surprisingly to me) top in every one of the last 12 full-field tournaments I've run, using the official scoring code (in the question body), and Rand-Margin Little Lisper. Has Casey changed something about the scoring? – jscs – 2011-05-02T04:04:14.133

3

Hyperrational Wasp

Implemented in Java because I wasn't sure how complex the data structures were going to end up. If this is a problem for someone then I think I can port it to bash without too many problems because in the end it only really uses simple associative arrays.

Note: I've removed this from a package in line with the latest version of my patch to the scorer to handle Java. If you want to post a Java solution which uses inner classes then you'll have to patch the patch.

import java.util.*;

public class HyperrationalWasp
{
    // I'm avoiding enums so as not to clutter up the warriors directory with extra class files.
    private static String Clam = "c";
    private static String Rat = "t";
    private static String Ambiguous = "x";

    private static final String PROLOGUE = "ttc";

    private static int n;
    private static String myActions;
    private static String hisActions;

    private static String decideMove() {
        if (n < PROLOGUE.length()) return PROLOGUE.substring(n, n+1);

        // KISS - rather an easy special case here than a complex one later
        if (mirrorMatch()) return Clam;
        if (n == 99) return Rat; // This is rational rather than superrational

        int memory = estimateMemory();
        if (memory == 0) return Rat; // I don't think the opponent will punish me
        if (memory > 0) {
            Map<String, String> memoryModel = buildMemoryModel(memory);
            String myRecentHistory = myActions.substring(0, memory - 1);
            // I don't think the opponent will punish me.
            if (Clam.equals(memoryModel.get(Rat + myRecentHistory))) return Rat;
            // I think the opponent will defect whatever I do.
            if (Rat.equals(memoryModel.get(Clam + myRecentHistory))) return Rat;
            // Opponent will cooperate unless I defect.
            return Clam;
        }

        // Haven't figured out opponent's strategy. Tit for tat is a reasonable fallback.
        return hisAction(0);
    }

    private static int estimateMemory() {
        if (hisActions.substring(0, n-1).equals(hisActions.substring(1, n))) return 0;

        int memory = -1; // Superrational?
        for (int probe = 1; probe < 5; probe++) {
            Map<String, String> memoryModel = buildMemoryModel(probe);
            if (memoryModel.size() <= 1 || memoryModel.values().contains(Ambiguous)) {
                break;
            }
            memory = probe;
        }

        if (memory == -1 && isOpponentRandom()) return 0;

        return memory;
    }

    private static boolean isOpponentRandom() {
        // We only call this if the opponent appears not have have a small fixed memory,
        // so there's no point trying anything complicated. This is supposed to be a Wilson
        // confidence test, although my stats is so rusty there's a 50/50 chance that I've
        // got the two probabilities (null hypothesis of 0.5 and observed) the wrong way round.
        if (n < 10) return false; // Not enough data.
        double p = count(hisActions, Clam) / (double)n;
        double z = 2;
        double d = 1 + z*z/n;
        double e = p + z*z/(2*n);
        double var = z * Math.sqrt(p*(1-p)/n + z*z/(4*n*n));
        return (e - var) <= 0.5 * d && 0.5 * d <= (e + var);
    }

    private static Map<String, String> buildMemoryModel(int memory) {
        // It's reasonable to have a hard-coded prologue to probe opponent's behaviour,
        // and that shouldn't be taken into account.
        int skip = 0;
        if (n > 10) skip = n / 2;
        if (skip > 12) skip = 12;

        Map<String, String> memoryModel = buildMemoryModel(memory, skip);
        // If we're not getting any useful information after skipping prologue, take it into account.
        if (memoryModel.size() <= 1 && !memoryModel.values().contains(Ambiguous)) {
            memoryModel = buildMemoryModel(memory, 0);
        }
        return memoryModel;
    }

    private static Map<String, String> buildMemoryModel(int memory, int skip) {
        Map<String, String> model = new HashMap<String, String>();
        for (int off = 0; off < n - memory - 1 - skip; off++) {
            String result = hisAction(off);
            String hypotheticalCause = myActions.substring(off+1, off+1+memory);
            String prev = model.put(hypotheticalCause, result);
            if (prev != null && !prev.equals(result)) model.put(hypotheticalCause, Ambiguous);
        }
        return model;
    }

    private static boolean mirrorMatch() { return hisActions.matches("c*ctt"); }
    private static String myAction(int idx) { return myActions.substring(idx, idx+1).intern(); }
    private static String hisAction(int idx) { return hisActions.substring(idx, idx+1).intern(); }
    private static int count(String actions, String action) {
        int count = 0;
        for (int idx = 0; idx < actions.length(); ) {
            int off = actions.indexOf(action, idx);
            if (off < 0) break;
            count++;
            idx = off + 1;
        }
        return count;
    }

    public static void main(String[] args) {
        if (args.length == 0) {
            hisActions = myActions = "";
            n = 0;
        }
        else {
            n = args[0].length();
            myActions = args[0].replaceAll("[KR]", Clam).replaceAll("[SE]", Rat);
            hisActions = args[0].replaceAll("[KS]", Clam).replaceAll("[RE]", Rat);
        }

        System.out.println(decideMove());
    }

}

The changes I made to the scorer to run this are:

17a18
> import re
22a24
> GCC_PATH = 'gcc'                #path to c compiler
24c26
< JAVA_PATH = '/usr/bin/java'   #path to java vm
---
> JAVA_PATH = '/usr/bin/java'     #path to java vm
50,55c52,59
<         elif ext == '.java':
<             if subprocess.call([JAVAC_PATH, self.filename]) == 0:
<                 print 'compiled java: ' + self.filename
<                 classname = re.sub('\.java$', '', self.filename)
<                 classname = re.sub('/', '.', classname);
<                 return JAVA_PATH + " " + classname
---
>         elif ext == '.class':
>             # We assume further down in compilation and here that Java classes are in the default package
>             classname = re.sub('.*[/\\\\]', '', self.filename)
>             dir = self.filename[0:(len(self.filename)-len(classname))]
>             if (len(dir) > 0):
>                 dir = "-cp " + dir + " "
>             classname = re.sub('\\.class$', '', classname);
>             return JAVA_PATH + " " + dir + classname
196c200,201
<         if os.path.isdir(sys.argv[1]):
---
>         warriors_dir = re.sub('/$', '', sys.argv[1])
>         if os.path.isdir(warriors_dir):
198,200c203,211
<             for foo in os.listdir("./src/"): # build all c/c++ champs first.
<                 os.system(str("gcc -o ./warriors/" + os.path.splitext(os.path.split(foo)[1])[0] + " ./src/" + foo ))
<                 #print str("gcc -o ./warriors/" + os.path.splitext(os.path.split(foo)[1])[0] + " ./src/" + foo )
---
>             for foo in os.listdir("./src/"): # build all c/c++/java champs first.
>                 filename = os.path.split(foo)[-1]
>                 base, ext = os.path.splitext(filename)
>                 if (ext == '.c') or (ext == '.cpp'):
>                     subprocess.call(["gcc", "-o", warriors_dir + "/" + base, "./src/" + foo])
>                 elif (ext == '.java'):
>                     subprocess.call([JAVAC_PATH, "-d", warriors_dir, "./src/" + foo])
>                 else:
>                     print "No compiler registered for ", foo
202,203c213,214
<             print "Finding warriors in " + sys.argv[1]
<             players = [sys.argv[1]+exe for exe in os.listdir(sys.argv[1]) if os.access(sys.argv[1]+exe,os.X_OK)]
---
>             print "Finding warriors in " + warriors_dir
>             players = [warriors_dir+"/"+exe for exe in os.listdir(warriors_dir) if (os.access(warriors_dir+"/"+exe,os.X_OK) or os.path.splitext(exe)[-1] == '.class')]

Thanks to @rmckenzie for folding in my challenger function.

Peter Taylor

Posted 2011-04-29T21:07:00.847

Reputation: 41 901

Just a matter of style.... should the .java file be considered "source" and moved to the ./src directory and the final .class placed in the ./warriors folder by the same subscript used on .c files, or is java interpreted and as such the .java and .class stay together? Nice changes to the scorer in any case... will have them in the repo stat. – arrdem – 2011-05-02T20:57:36.073

@rmckenzie, good point: yes, technically it's compiled. The reason I had the source file in the warriors directory is that the python files are there too - and they're compiled. If you want I can check what changes are required to compile it from ./src to ./warriors - but it's going to require a few compiler arguments, because Java by default assumes that the directory structure reflects the package (namespace). – Peter Taylor – 2011-05-02T21:26:45.990

@peter, I was just wondering... the warriors are found in ./warriors by virtue of being *nix 777, or otherwise executable. The Python and Lisp scripts are NOMINALLY compiled for performance, but are executable in their natural (source) state. TO MY KNOWLEDGE AS A NON-JAVA PERSON, .java files don't have those permissions and hence won't show up. That's what the c hack exists for... because compilation is a separate step. So yeah. I would greatly appreciate it if you would look into making that change. REPO LINK

– arrdem – 2011-05-02T21:30:33.303

Using your code and a chmod 777'd wasp, the JVM threw this beauty.Exception in thread "main" java.lang.NoClassDefFoundError: //warriors/HyperrationalWasp Caused by: java.lang.ClassNotFoundException: ..warriors.HyperrationalWasp at java.net.URLClassLoader$1.run(URLClassLoader.java:217) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:205) at java.lang.ClassLoader.loadClass(ClassLoader.java:321) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:294) at java.lang.ClassLoader.loadClass(ClassLoader.java:266) – arrdem – 2011-05-02T21:55:03.303

@rmckenzie, that's odd. Anyway, I think I'll have a patch for you very shortly. I've had to hack the loading code, because class files aren't executable. And if any other Java entries use inner classes it will break. – Peter Taylor – 2011-05-02T22:11:37.010

@rmckenzie, supplied as a patch against your REPO as of 20 minutes ago. – Peter Taylor – 2011-05-02T22:20:19.603

Okay. Thanks for the code, I made the updates... however I found a bug in your source code that makes automagically building it via score.py a real bitch. If you would be so kind, can you ditch the package statement? Causes all kinds of hell with javac expecting a folder layout, and it doesn't add to the code's functionality. edit: I made this change already, I'm just updating you as it's you software. – arrdem – 2011-05-02T23:56:08.893

@rmckenzie, check the edit history of this post. I already did that when I added the patch. – Peter Taylor – 2011-05-03T06:19:12.320

heh. I'm slow on the uptake, my apologies. – arrdem – 2011-05-03T17:54:29.893

I haven't been paying attention to this for a few days, and haven't gotten around to running your Wasp. How is it doing in your trials? I'm quite impressed with the idea; I'd give it another +1 if I could, for being the most artificially-intelligent strategy if nothing else. – jscs – 2011-05-04T19:42:54.157

@Josh, it was winning most of my trials until Joey created Random sucker. But that's a bad matchup for both of us, and a good matchup for gradual and druid, so I'm now tending to come in 4th. I need to tweak a bit to try to handle soft_majo well too - my prologue means that that ends up being mutual destruction when fundamentally both warriors are fairly cooperative. – Peter Taylor – 2011-05-04T21:11:51.093

3

Soft_majo

Ah well, another one of the standard strategies, just to complete the line-up.

This one picks the move the opponent has made most; if equal it cooperates.

#include <stdio.h>
#include <string.h>

int main(int argc, char * argv[]) {
    int d = 0, i, l;

    if (argc == 1) {
        printf("c\n");
    } else {
        l = strlen(argv[1]);

        for (i = 0; i < l; i++)
            if (argv[1][i] == 'R' || argv[1][i] == 'E')
                d++;

        printf("%c\n", d > l/2 ? 't' : 'c');
    }
}

Joey

Posted 2011-04-29T21:07:00.847

Reputation: 12 260

Your code is soft_majo, but your description is hard_majo. – Peter Taylor – 2011-05-03T12:59:26.270

Peter: Eek, sorry; fixed. – Joey – 2011-05-03T13:18:13.760

3

Random sucker

This one will defect if the opponent defects too often (threshold), but will randomly try backstabbing every now and then.

Does fairly well against everyone except the Java and Lisp players (which I cannot get to run, due to neither Java nor Lisp on the test machine); most of the time at least.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define THRESHOLD 7
#define RAND 32

int main(int c, char * a []) {
    int r;
    char * x;
    int d = 0;

    srandom(time(0) + getpid());

    if (c == 1) {
        printf("c\n");
        return 0;
    }

    for (x = a[1]; *x; x++)
        if (*x == 'R' || *x == 'E') d++;

    if (d > THRESHOLD || random() % 1024 < RAND || strlen(a[1]) == 99)
        printf("t\n");
    else
        printf("c\n");

    return 0;
}

Joey

Posted 2011-04-29T21:07:00.847

Reputation: 12 260

Against HyperrationalWasp it will generally do roughly as against the devil. It starts out just cooperating all the time, so I assume it's the angel and go on the attack. Then when it hits the threshold you'll switch into devil mode and I'll switch into t4t. If it randomly backstabs in the first 6 moves then I'll switch into t4t before you switch into devil, but the odds of that aren't high. – Peter Taylor – 2011-05-04T11:59:48.180

1Peter: Well, I rarely test the strategies directly against each other, as the overall field has quite some influence on strategy performance. Currently it mostly battles with gradual and druid for the first place in my tests. – Joey – 2011-05-04T12:11:10.503

Gradual and druid both score about 200 against Wasp; random sucker will score about 83. – Peter Taylor – 2011-05-04T12:17:50.833

2

BYGONES

#!/usr/bin/env python

"""
BYGONES, entry to 1P5 Iterated Prisoner's Dilemma, by Josh Caswell

Cooperates at first, plays as Tit for Tat for `bygones * 2` rounds, then checks 
history: if there's too much ratting, get mad and defect; too much 
suckering, feel bad and cooperate.
"""

bygones = 5

import sys

# React to strangers with trust.
try:
    history = sys.argv[1]
except IndexError:
    print 'c'
    sys.exit(0)

replies = { 'K' : 'c', 'S' : 'c',
            'R' : 't', 'E' : 't' }

# Reply in kind.
if len(history) < bygones * 2:
    print replies[history[0]]
    sys.exit(0)

# Reflect on past interactions.
faithful_count = history.count('K')
sucker_count = history.count('S')
rat_count = history.count('R')

# Reprisal. 
if rat_count > faithful_count + bygones:
    # Screw you!
    print 't'
    sys.exit(0)

# Reparation.
if sucker_count > faithful_count + bygones:
    # Geez, I've really been mean.
    print 'c'
    sys.exit(0)

# Resolve to be more forgiving.
two_tats = ("RR", "RE", "ER", "EE")
print 't' if history[:2] in two_tats else 'c'

Haven't worked out the best value for bygones yet. I don't expect this to be a winning strategy, but I'm interested in the performance of a strategy something like what I think is "good" in real life. A future revision may include checking the number of mutual defections, too.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

2

Help Vampire

#!/usr/bin/env python

"""
Help Vampire, entry to 1P5 Iterated Prisoner's Dilemma,
by Josh Caswell.

1. Appear Cooperative 2. Acknowledge Chastisement 
3. Act contritely 4. Abuse charity 5. Continual affliction
"""

import sys
from os import urandom

LEN_ABASHMENT = 5

try:
    history = sys.argv[1]
except IndexError:
    print 'c'    # Appear cooperative
    sys.exit(0)

# Acknowledge chastisement
if history[0] in "RE":
    print 'c'
# Act contritely
elif set(history[:LEN_ABASHMENT]).intersection(set("RE")):
    print 'c'
# Abuse charity
elif history[0] == 'S':
    print 't'
# Continual affliction
else:
    print 't' if ord(urandom(1)) % 3 else 'c'

Has an amusingly asymmetrical result when pitted against itself. If only this solution could be applied in real life.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

2

Druid

#!/usr/bin/env python

"""
Druid, by Josh Caswell

Druids are slow to anger, but do not forget.
"""

import sys
from itertools import groupby

FORBEARANCE = 7
TOLERANCE = FORBEARANCE + 5

try:
    history = sys.argv[1]
except IndexError:
    history = ""

# If there's been too much defection overall, defect
if (history.count('E') > TOLERANCE) or (history.count('R') > TOLERANCE):
    print 't'
# Too much consecutively, defect
elif max([0] + [len(list(g)) for k,g in     # The 0 prevents dying on []
                groupby(history) if k in 'ER']) > FORBEARANCE:
    print 't'
# Otherwise, be nice
else:
    print 'c'

Does reasonably well against the base roster.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

2

Little Schemer

#!/usr/bin/env python

"""
The Little Schemer, by Josh Caswell

No relation to the book. Keeps opponent's trust > suspicion 
by at least 10%, trying to ride the line.
"""

from __future__ import division
import sys
from os import urandom

out = sys.stderr.write

def randrange(n):
    if n == 0:
        return 0
    else:
        return ord(urandom(1)) % n

try:
    history = sys.argv[1]
except IndexError:
    print 'c'
    sys.exit(0)

R_count = history.count('R')
S_count = history.count('S')
K_count = history.count('K')
E_count = history.count('E')

# Suspicion is _S_ and E because it's _opponent's_ suspicion
suspicion = (S_count + E_count) / len(history)
# Likewise trust
trust = (K_count + R_count) / len(history)

if suspicion > trust:
    print 'c'
else:
    projected_suspicion = (1 + S_count + E_count) / (len(history) + 1)
    projected_trust = (1 + K_count + R_count) / (len(history) + 1)

    leeway = projected_trust - projected_suspicion
    odds = int(divmod(leeway, 0.1)[0])

    print 't' if randrange(odds) else 'c'

Does poorly against the base set, but quite well against its target. Obviously, not written in Scheme.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

Why do I sense a challenge? – arrdem – 2011-05-01T14:47:23.197

Defeated this bugger.... randomized the threshold in Lisper. – arrdem – 2011-05-01T16:37:56.257

@rmckenzie: but how did that affect your play against the rest of the field? With enough cooperators working with each other, paranoid or envious strategies will start to do worse. You've still got a fixed upper limit, too, which could be exploited. – jscs – 2011-05-01T17:35:07.107

If you read through the current lisper, it's more defensive than envious. It tries to detect opponents who are pursuing statistically treacherous strats like this, and only then returns fire. It's CC opening is designed to get off on the right foot with Thieves, and has the added benefit of convincing most of the cooperative strats to play along. – arrdem – 2011-05-01T17:43:16.533

@rmckenzie: Very good! I'll give it a spin. – jscs – 2011-05-01T17:56:09.997

@rmckenzie: I'm still seeing Little Schemer beat Random-Margin Little Lisper head-to-head every time (by around 100 points to 250). It's lower in the overall ranking, of course. Do you see different results? – jscs – 2011-05-02T04:02:07.770

2

Simpleton

#!/usr/bin/env python

"""
Simpleton, by Josh Caswell

Quick to anger, quick to forget, unable to take advantage of opportunity.
"""

import sys
from os import urandom

WHIMSY = 17

try:
    history = sys.argv[1]
except IndexError:
    if not ord(urandom(1)) % WHIMSY:
        print 't'
    else:
        print 'c'
    sys.exit(0)

if history[0] in "RE":
    print 't'
elif not ord(urandom(1)) % WHIMSY:
    print 't'
else:
    print 'c'

Does okay against the base roster.

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

1

Tit for Two Tats

another old favorite

#!/usr/bin/env python

"""
Tit For Two Tats, entry to 1P5 Iterated Prisoner's Dilemma, 
    by Josh Caswell (not an original idea).

Cooperates unless opponent has defected in the last two rounds.
"""

import sys
try:
    history = sys.argv[1]
except IndexError:
    history = ""

two_tats = ("RR", "RE", "ER", "EE")

if len(history) < 2:
    print 'c'
else:
    print 't' if history[:2] in two_tats else 'c'

jscs

Posted 2011-04-29T21:07:00.847

Reputation: 900

You can't do a return unless you're inside a function. Maybe use sys.exit(0) ? Or just let it finish. Edit: Also the first invocation to your program is without any history which causes an IndexError when you do argv[1]. – Casey – 2011-04-30T01:42:24.180

You may have left out the len(history)<2 clause, because that last one looks like the else part. – dmckee --- ex-moderator kitten – 2011-04-30T01:44:01.730

@Casey @dmckee Thank you for the bug fixes. "Duh" on me for return especially! – jscs – 2011-04-30T01:47:04.973

@dmckee: This started out as part of a more complicated thingy, and then I realize I had re-written Tit for Two Tats and decided to enter that. Copy-paste user error. – jscs – 2011-04-30T01:48:55.580

@Josh: I saw your Bygones entry briefly, did you delete it? It looked interested. – Casey – 2011-04-30T01:55:02.237

@Casey: yes, I realized it had many of the same troubles. It'll be back up soonish. Thanks! – jscs – 2011-04-30T01:57:31.520