#! /usr/bin/python __author__ = 'mjs' __version__ = 'V3.0' __date__ = '22 Feb 2006' doc=''' Harrigan %s: LOAPS Referee Program for the 2005 POTM LOAPS contest. usage: harrigan {opt} -b| Initial board setup file. -h Help text (you are reading it). -m Run a match (Play two games alternating sides). -n Run this number of games or matches. -p Prefix to log filenames (eg: "logs\\" or "XvsY_") -r[] Randomly move N pieces or total random board each game. -t Time limit for players default=60 seconds. A player program may have the name "human". This will tell the referee to prompt the user for moves. The human moves are not timed. The board graphics only work with a monospaced font in an editor or terminal. The previous move is shown with a dot where the piece came from, and angle brackets around the piece's new position. The program writes a log file in the same format, so you can flip through a game in an editor by making the window size the same size as the board output and using the PgDn and PgUp keys to view the match. An optional file can be specified on the command line. A boardfile is a standard 7 line LOAPS datafile that sets initial starting positions. A game file is a standard 10 line LOAPS datafile that can be used to test programs with interesting game scenarios. The turn number, time remaining, scores, and board positions can be set. v1.0: 10th Nov 2005 by mjs. v2.0: 16th Nov 2005 by mjs. v2.2: 30th Nov 2005 by mjs. Added N random moves option. v3.0: 22nd Feb 2006 by mjs. Made random moves same for each player1. Change tmp move file to move.ldf (loaps data file). Using time.clock() for non-posix OSs (uses wall clock I think...). Added more statistics. Now reports who connected and with how many peices left. ''' % __version__ import sys, time, os, operator from string import * from random import randint, choice # Set up logging function: log = None def wr( msg, logname=None ): # Write a message to the log and stdout: global log if logname: log = open( logname, 'w' ) log.write( '%s\n' % msg ) sys.stdout.write( '%s\n' % msg ) # General purpose class for data management: class obj: def __init__(s,**kw): s.__dict__.update(kw) def upd(s,*args,**kw): [ s.__dict__.update(z) for z in args+(kw,) ] def __repr__(s): return `s.__dict__` # General purpose data structures: directions = [(dx,dy) for dx in (-1,0,1) for dy in (-1,0,1) if (dx,dy)!=(0,0)] squares = [ (x,y) for x in range(7) for y in range(7) ] # Function definitions: def inter(s): return [eval(z) for z in split(s)] def grok(f,s): return dict( zip(split(f),inter(s)) ) def isconnected( board, player ): # Return true if all pieces of this player are connected (inefficiently!) pieces = [ p for p in squares if board[p]==player ] links = { pieces[0]:1 } for i in range(7): w = [ (x+dx,y+dy) for x,y in links for dx,dy in directions ] links.update( dict( [ (p,1) for p in w if board.get(p)==player ] ) ) return len(links) == len(pieces) or len(pieces) == 1 def checkmove( board, src, dest, player ): # Return true if this player can legally move piece from src to dest if player != board.get( src ): return 0 opponent = (1,2)[player==1] x,y = src moves = [] for dx,dy in [(1,0),(1,1),(0,1),(-1,1)]: num = len( [ 1 for m in range(-6,7) if board.get((x+m*dx,y+m*dy)) in (1,2) ] ) for ax,ay in [(dx,dy),(-dx,-dy)]: good = 1 for step in range(1,num): if board.get((x+ax*step,y+ay*step)) in (opponent,None): good=0 value = board.get((x+ax*num,y+ay*num)) if good and value not in (player,None): moves.append( (x+ax*num,y+ay*num) ) return (dest in moves) def show( oldboard, titles, prop1, prop2, src, dest ): # Show a display of the board and player properties bar = '|---+---+---+---+---+---+---|' border = bar.replace('-','=') outp = [ ' '+border ] board = oldboard.copy() if src: board[src] = 3 for y2 in range(14): y = y2 / 2 if y2 % 2 == 1: outp.append( ' '+bar ) else: row = [ (' %s ','>%s<')[(x,6-y)==dest]%(' X@.')[board[x,6-y]] for x in range(7) ] outp.append( ' %u |%s|' % (7-y,('|'.join(row))) ) if y2 < len(titles): outp[-1] += ' '+strip( ('%-10s' % titles[y2]) + prop1[y2] + prop2[y2] ) outp[-1] = ' '+border outp.append( ' a b c d e f g' ) wr( '\n'.join(outp) ) boardinuse = None def game( progs, opt, rev=None ): # Run a single game and return a tuple of who won and why. # Input data structures: global boardinuse note = '' if rev: progs = [progs[1],progs[0]] playlist = [ obj(name=n,num=i,sym='-X@'[i]) for i,n in zip((1,2),progs) ] for player in playlist: player.upd( score=0, time=float(opt.get('t',60)) ) whoturn, turn = 1, 1 if opt.get('b'): data = open( opt['b'] ).readlines() else: data = split( ''' 0110110 2000002 2000002 0000000 2000002 2000002 0110110 ''' ) if len(data) == 7 : recbrd = [ z.replace('.','0') for z in data ] board = dict( [ ((x,y),int(recbrd[6-y][x])) for x,y in squares ] ) elif len(data) == 10 : whoturn, turn = inter( data[0] ) p1, p2 = playlist p1.upd( grok( 'num score time', data[1] ) ) p2.upd( grok( 'num score time', data[2] ) ) board = dict([ ((x,y),int(data[9-y].replace('.','0')[x])) for x,y in squares ]) if 'r' in opt: if rev and boardinuse: # Use stored board if this is reverse game (22feb06) board = boardinuse.copy() elif opt['r']: # N for perturb by N pieces... for i in range( int(opt['r']) ): pos = choice( [p for p,v in board.items() if v >= 1] ) newpos = (randint(0,6), randint(0,6)) board[ newpos ] = board[ pos ] board[ pos ] = 0 else: # otherwise randomise the whole board... board = dict( [(p,0) for p in squares] ) for i in range(8): for n in (1,2): board[ (randint(0,6), randint(0,6)) ] = n boardinuse = board.copy() # Run game: titles = split( 'Player: Piece: Name: Points: Time:' ) props = split( 'num sym name score time' ) done = 0 src, dest = None, None while 1: for j in (1,2): p,o = playlist if whoturn == 2 and j == 1 : whoturn = None # skip player 1 if we start on player 2 turn... continue if j==2: p,o = o,p # print board wr( '\nTurn:%s Player%s to move' % (turn,j) ) prop1, prop2 = [ ['%-15s'%eval('z.'+a) for a in props] for z in playlist ] show( board, titles, prop1, prop2, src, dest ) # write board to file data = '%u %u' % (j,turn) data2=['%u %u %f'%(k+1,playlist[k].score,playlist[k].time) for k in (0,1)] data3 = [ ''.join([`board[x,6-y]` for x in range(7)]) for y in range(7)] data3 = [ z.replace('0','.') for z in data3 ] open('move.ldf','w').write( '\n'.join([data]+data2+data3) + '\n' ) while 1: # get move from program or human # the loop is to catch human mistakes and allow another try... # TODO: Add timeout and better time keeping here... timetook = 0.1 # Default human time #print 'Calling player...' if p.name == 'human': move = raw_input( 'Move? ' ) else: if os.name=='posix': start = os.times()[2] else: start = time.clock() move = strip( os.popen( p.name + ' move.ldf' ).read() ) if os.name=='posix': timetook = os.times()[2] - start else: timetook = time.clock() - start try: src, dest = [ ( 'abcdefg'.index(z[0]), int(z[1])-1 ) for z in split(move) ] except: wr( 'Player%u - Bad move format: "%s"' % (p.num,move) ) if p.name != 'human': return o.name, o.score, p.score, turn, 'BadOutput' else: continue if not checkmove( board, src, dest, p.num ): wr( 'Player%u - Illegal move: "%s"' % (p.num,move) ) if p.name != 'human': return o.name, o.score, p.score, turn, 'BadMove' else: continue break # update player score and time score = { (3,3):7, (1,1):3, (1,5):3, (5,1):3, (5,5):3 }.get(dest,0) if board[dest] == o.num: score += 1 p.score += score p.time -= timetook # if move: update board board[src] = 0 board[dest] = p.num p.pcount = board.values().count(p.num) o.pcount = board.values().count(o.num) # if error, time, or bad move: report error, end if p.time < 0: wr( 'Player%u - Exceeded time limit' % p.num ) return o.name, o.score, p.score, turn, 'TimeLimit' # if connected: report winner, end if isconnected( board, p.num ): p.score += 12 score += 12 done = 1 note += ' Player %s connected (%u vs %u pieces)' % (p.name,p.pcount,o.pcount) elif isconnected( board, o.num ): o.score += 12 done = 1 note += ' Player %s forced connect (%u vs %u pieces)' % (p.name,p.pcount,o.pcount) # print move, score, time msgfmt = 'Player%u %s moves %s (%s points in %s seconds)' wr( msgfmt % ( p.num, p.name, move, score, timetook ) ) if done: break if done or turn >= 50: break turn += 1 # print results wr( '\nFINAL RESULT at Turn:%s Player%s moved last' % (turn,j) ) prop1, prop2 = [ ['%-15s'%eval('z.'+a) for a in props] for z in playlist ] show( board, titles, prop1, prop2, src, dest ) p1, p2 = playlist if p1.score < p2.score : p1,p2 = p2,p1 scoretext = '(%u to %u points)' % (p1.score,p2.score) if turn >= 50: wr( 'Player1 and Player2 have a draw at 50 moves '+scoretext ) note = 'Move50'+note elif p1.score == p2.score: wr( 'Player1 and Player2 have a draw with equal points '+scoretext ) note = 'Draw'+note else: wr( 'WINNER is Player%u %s %s' % (p1.num, p1.name, scoretext) ) note = 'Win'+note wr( '=.= '*15 + '\n' ) return p1.name, p1.score, p2.score, turn, note ########### MAIN ########## # Get args and options: opt = dict([ (arg[1:2],arg[2:]) for arg in sys.argv[1:] if arg[0]=='-']) progs = [arg for arg in sys.argv[1:] if arg[0]!='-'] if len(progs) != 2 or 'h' in opt: print doc sys.exit(0) # Run all games (or matches): stats = [] for gnum in range( 1, int(opt.get('n',1))+1 ): logname = '%sgame%u.log' % ( opt.get('p',''), gnum ) wr( 'Harrigan Referee: LOAPS game %s vs %s'%tuple(progs), logname ) stats.append( (gnum,)+game( progs, opt ) ) if 'm' in opt: stats.append( (gnum,)+game( progs, opt, rev=1 ) ) # Produce the statistics file header: player = dict( [(n,obj(name=n, wins=0, wpoints=0, wturns=0, draws=0, points=0, sumdif=0, turns=0)) for n in progs] ) st = open( '%sstats.txt' % opt.get('p',''), 'w' ).write st( 'Harrigan LOAPS Match Summary\n' ) st( '%s\n' % time.ctime() ) st( 'Cmdline: "%s"\n\n' % ' '.join(sys.argv) ) namesize = max( [10]+[len(p) for p in progs] ) + 1 namefmt = '%-'+`namesize`+'s ' fmt = '%3s '+namefmt+'%12s %12s %7s %s\n' st( fmt % ('NUM','WINNER','WINNERSCORE','LOSERSCORE','TURN','OUTCOME') ) # Print game records: for rec in stats: st( fmt % rec ) # Gather statistics: for rec in stats: p1,p2 = player.values() if p2.name == rec[1]: p2,p1 = p1,p2 p1.turns += rec[4] p2.turns += rec[4] p1.points += rec[2] p2.points += rec[3] if rec[5].split()[0] in ('Draw','Move50'): p1.draws += 1 p2.draws += 1 else: p1.sumdif += rec[2] - rec[3] p1.wins += 1 p1.wpoints += rec[2] p1.wturns += rec[4] # Print statistics for each player: for progname in progs: p = player[ progname ] st('\nPlayer %s Statistics\n' % p.name) st('LOAPS Score: %u\n' % (p.wins*2 + p.draws) ) st('Wins: %.1f%%\n' % (100.0 * p.wins / len(stats)) ) st('Points per win: %.1f\n' % (1.0 * p.wpoints / max(p.wins,0.1)) ) st('Point margin per win: %.1f\n' % (1.0 * p.sumdif / max(p.wins,0.1)) ) st('Turns per win: %.1f\n' % (1.0 * p.wturns / max(p.wins,0.1)) ) st('Points per turn: %.1f\n' % (1.0 * p.points / max(p.turns,0.1)) )