""" Scenario for cooperative conquest game:: usage: teamofrivals.py [-h] [-p] [-n NUMBER] [-1] [-f FILE] [-u] [-q] [-m | -a] [--additive | --restorative | --minimal | --none] map positional arguments: map XML file containing world map optional arguments: -h, --help show this help message and exit -p, --predict print out predictions before stepping [default: False] -n NUMBER, --number NUMBER Number of games to play [default: 1] -1, --single stop execution after one round [default: False] -f FILE, --file FILE name of scenario file [default: <map root>.psy] -u, --update update scenario file [default: False] -q, --quiet suppress all output [default: False] Decision Mode: -m, --manual enter actions manually -a, --auto agents choose actions autonomously [default] Resource Generation: --additive Resources from all territories, unused ones kept --restorative Resources from all territories, unused ones lost [default] --minimal Resources from initial territories, unused ones lost --none Resources from winning only, unused ones kept Input: ====== Map file -------- XML file containing regions, specifying their individual parameters and neighbors. If omitted, the "value" attribute defaults to 5, as does the "occupants" attribute". The "owner" attribute indicates which player, 1-N, owns the territory at the start of the game. The default is 0, which indicates that the enemy side owns the territory:: <map> <region name="Alaska" value="5" occupants="2" owner="0"> <neighbor name="Northwest Territory"/> <neighbor name="Alberta"/>" <neighbor name="Kamchatka"/>" </region> <region> . . . Output: ======= Round number ------------ Player: how many resources this player currently owns which territories this player currently owns Enemy: which territories are still currently owned by the enemy (number of defenders in each territory in parens) Player actions: which territories the player has chosen to invade (number of armies allocated in parens) Optional Predictions: Territory being invaded (how many resources are gained by the owner) The player(s) who would be the owner if the invasion is successful The probability that the invasion will be successful Results (overall probability of outcome) Territory being invaded: the result of the invasion (spinner result of individual invasion, *not* probability of individual outcome) """ from argparse import ArgumentParser import csv from datetime import datetime import os.path import random import sys import lxml.etree as ET import time from psychsim.pwl import * from psychsim.world import * from psychsim.agent import Agent from psychsim.reward import * class ResourceWorld(World): """ @cvar nullAgent: string label that means none of the ResourceAgent members of this world @ivar carryOver: if C{True}, then unallocated resources carry over to next round (default is C{False}) @type carryOver: bool @ivar floor: minimum number of resources that a player can have (default is no minimum) @type floor: int """ nullAgent = '__none__' memory = False def __init__(self,xml=None,allocateVerb=None,allocationState=None,winnerState=None,floor=None): self.allocators = set() self.objects = [] self.carryOver = True self.resourceName = None self.agentOrder = [] World.__init__(self,xml) if xml is None: self.allocateVerb = allocateVerb self.allocationState = allocationState self.winnerState = winnerState def addAgent(self,agent): World.addAgent(self,agent) if isinstance(agent,ResourceAgent): if self.resourceName is None: self.resourceName = agent.resourceName self.allocators.add(agent) for obj in agent.objects: if self.agents.has_key(obj) and not self.agents[obj] in self.objects: self.objects.append(self.agents[obj]) else: for other in self.allocators: if agent.name in other.objects and not agent in self.objects: self.objects.append(agent) def getResources(self,state=None): """ @return: a table of amount of resources owned by each player @rtype: strS{->}int """ if state is None: state = self.state[None] resources = {} for agent in self.agents.values(): if isinstance(agent,ResourceAgent): resources[agent.name] = self.getState(agent.name,agent.resourceName).expectation() return resources def getOwnership(self,state=None): """ @return: a table of territories owned by each agent @rtype: strS{->}set(str) """ if state is None: state = self.state[None] ownership = {} # Hacky way to figure out what can be owned for agent in self.agents.values(): if isinstance(agent,ResourceAgent): objects = agent.objects break for obj in objects: # Who owns it? owner = self.getState(obj,'owner',state) assert len(owner) == 1 owner = owner.domain()[0] # Add to table try: ownership[owner].add(obj) except KeyError: ownership[owner] = {obj} return ownership def getTotalValue(self,feature,state=None): """ @param feature: the name of the state feature containing the value to be totaled @type feature: str @return: a table of total value of territories owned by each agent @rtype: strS{->}int """ if state is None: state = self.state[None] result = {} # Hacky way to figure out what can be owned for agent in self.agents.values(): if isinstance(agent,ResourceAgent): objects = agent.objects break for agent in self.agents.values(): if isinstance(agent,ResourceAgent): # This is a player, make sure it has an entry if not result.has_key(agent.name): result[agent.name] = 0 elif agent.name in objects: # This is something owned, add its value to the owner's entry owner = self.getState(agent.name,'owner',state) value = self.getState(agent.name,feature,state) assert len(owner) == 1 owner = owner.domain()[0] try: result[owner] += value.expectation() except KeyError: result[owner] = value.expectation() return result def predictResult(self,actions): """ @param actions: the (sub)set of actions to predict the results of @type actions: L{ActionSet} @return: a dictionary of predictions in dictionary form for each object included in the action: - leader: prediction of which allocator would own the object, if successful - winner: prediction of who owns the object, including possibility of being unsuccessful @rtype: str{S->}str{S->}L{Distribution}(str) """ # Collect targets objects = {} for name,action in actions.items(): for atom in action: try: objects[atom['object']].add(atom) except KeyError: objects[atom['object']] = set([atom]) for obj in objects.keys(): objects[obj] = {'actions': ActionSet(objects[obj])} keys = {stateKey(obj,'invader'),stateKey(obj,'owner')} outcomes = self.step(objects[obj]['actions'],real=False,keys=keys) assert len(outcomes) == 1 objects[obj]['leader'] = self.getState(obj,self.winnerState,outcomes[0]['new']) objects[obj]['winner'] = self.getState(obj,'owner',outcomes[0]['new']) return objects def deltaOrder(self,actions,vector): return None def next(self,vector=None): if vector is None: assert len(self.state[None]) == 1,'Ambiguous state vector' vector = self.state[None].domain()[0] if self.getValue('phase',vector) == 'generate': return [o.name for o in self.objects] else: return [name for name in self.agents.keys() if isinstance(self.agents[name],ResourceAgent)] def getDynamics(self,key,action,state=None): if isTurnKey(key): # Caching doesn't work too well with partial prediction return [] elif isinstance(action,ActionSet) or isinstance(action,list): if key[-len(self.allocationState):] == self.allocationState: # Figure out the resulting allocation total = 0 for atom in action: if atom['verb'] == self.allocateVerb and \ key[:len(atom['object'])] == atom['object']: # Someone is allocating resources relevant to this state feature total += atom['amount'] if total > 0: return [makeTree(setToConstantMatrix(key,total))] else: return [] elif key[-len(self.resourceName):] == self.resourceName: # Figure out player resources if self.getValue('phase',state) == 'generate': player = key[:-(len(self.resourceName)+3)] total = 5 # Minimum value for obj in self.objects: if self.getValue(stateKey(obj,'owner'),state) == player and \ self.getValue(stateKey(obj,'invader'),state) == player: # I just won this territory total += self.getValue(stateKey(obj,'value'),state) tree = makeTree(incrementMatrix(key,total)) return [tree.desymbolize(self.symbols)] else: trees = [] for atom in action: if atom['verb'] == self.allocateVerb and \ key[:len(atom['subject'])] == atom['subject']: # The relevant player is allocating resources if self.carryOver: trees.append(makeTree(incrementMatrix(key,-atom['amount']))) else: trees.append(makeTree(setToConstantMatrix(key,0))) return trees elif key[-5:] == 'owner': # Figure out the probability of winning total = 0 for atom in action: if atom['verb'] == self.allocateVerb and \ key[:len(atom['object'])] == atom['object']: # Someone is allocating resources relevant to this state feature obj = atom['object'] total += atom['amount'] if total == 0: # No one touching this object return [] else: # Find ratio of invaders to defenders (hack warning!) if state is None: state = self.state[None].domain()[0] assert len(self.state[None]) == 1,'Unable to hack dynamics in uncertain states' denominator = total + state[stateKey(obj,'occupants')] winning = float(total)/float(denominator) return [makeTree({'distribution': [(setToFeatureMatrix(key,stateKey(obj, self.winnerState)),winning), (noChangeMatrix(key),1.-winning)]})] elif self.winnerState and key[-len(self.winnerState):] == self.winnerState: # Figure out who's allocating the most amounts = {} for atom in action: if atom['verb'] == self.allocateVerb and \ key[:len(atom['object'])] == atom['object']: # Someone is allocating resources relevant to this state feature try: amounts[atom['amount']].append(atom['subject']) except KeyError: amounts[atom['amount']] = [atom['subject']] if len(amounts) > 0: winners = amounts[max(amounts.keys())] if len(winners) == 1: # Phew, unique return [makeTree(setToConstantMatrix(key,winners[0]).desymbolize(self.symbols))] else: # Choose randomly among them tree = makeTree({'distribution': \ [(setToConstantMatrix(key,winner),1./float(len(winners))) \ for winner in winners]}) return[tree.desymbolize(self.symbols)] else: # Here we go hacking again if state is None: assert len(self.state[None]) == 1,'Unable to hack dynamics in uncertain states' state = self.state[None].domain()[0] if self.getValue('phase',state) == 'allocate': # No one allocating to this object, so no possible winner tree = makeTree(setToConstantMatrix(key,self.nullAgent)) return [tree.desymbolize(self.symbols)] else: return [] elif key == stateKey(None,'phase'): for atom in action: if atom['verb'] == self.allocateVerb: # If we allocate, then the phase becomes generate tree = makeTree(setToConstantMatrix(key,'generate')) return [tree.desymbolize(self.symbols)] elif atom['verb'] == 'generate': # If we generate, then the phase becomes allocate tree = makeTree(setToConstantMatrix(key,'allocate')) return [tree.desymbolize(self.symbols)] else: # No actions? Must be that no one allocated tree = makeTree(setToConstantMatrix(key,'generate')) return [tree.desymbolize(self.symbols)] elif key[-9:] == 'territory': if state is None: assert len(self.state[None]) == 1,'Unable to hack dynamics in uncertain states' state = self.state[None].domain()[0] if self.getValue('phase',state) == 'generate':#CVM changed world to self player = key[:-12] count = len([atom for atom in action \ if self.getValue(stateKey(atom['subject'],'owner'),state) == player]) return [makeTree(setToConstantMatrix(key,count))] else: return [] elif key[-5:] == 'value' and isinstance(self.agents[key[:-8]],ResourceAgent): if state is None: assert len(self.state[None]) == 1,'Unable to hack dynamics in uncertain states' state = self.state[None].domain()[0] if self.getValue('phase',state) == 'generate':#CVM changed world to self player = key[:-8] count = sum([self.getValue(stateKey(atom['subject'],'value')) for atom in action \ if self.getValue(stateKey(atom['subject'],'owner'),state) == player]) return [makeTree(setToConstantMatrix(key,count))] else: return [] dynamics = World.getDynamics(self,key,action,state) return dynamics def __xml__(self): doc = World.__xml__(self) doc.documentElement.setAttribute('verb',self.allocateVerb) doc.documentElement.setAttribute('allocation',self.allocationState) doc.documentElement.setAttribute('winner',self.winnerState) for obj in self.objects: node = doc.createElement('object') node.setAttribute('name',obj.name) doc.documentElement.appendChild(node) return doc def parse(self,element): World.parse(self,element,ResourceAgent) self.allocateVerb = str(element.getAttribute('verb')) self.allocationState = str(element.getAttribute('allocation')) self.winnerState = str(element.getAttribute('winner')) node = element.firstChild self.objects = [] while node: if node.nodeType == node.ELEMENT_NODE: if node.tagName == 'object': self.objects.append(self.agents[str(node.getAttribute('name'))]) node = node.nextSibling class ResourceAgent(Agent): """ @ivar allocateAll: if C{True}, then agent cannot leave resources unallocated (default is C{False}) """ def __init__(self,name,resource=None,verb=None,objects=None): Agent.__init__(self,name) if not resource is None: self.resourceName = resource self.verbName = verb self.objects = objects self.objectLegality = {} for obj in objects: self.objectLegality[obj] = makeTree(True) self.allocateAll = False def getActions(self,vector): targets = [] resources = self.world.getValue(stateKey(self.name,self.resourceName),vector) for obj in self.legalObjects(vector): targets.append(obj) actions = self.getCombos(targets,resources) return Agent.getActions(self,vector,actions).union(Agent.getActions(self,vector)) def hasAction(self,atom): if atom['subject'] == self.name and atom['verb'] == self.verbName and \ atom['object'] in self.objects: return True else: return Agent.hasAction(self,atom) def getCombos(self,targets,resources): if len(targets) == 0: return set([ActionSet()]) elif len(targets) == 1 and self.allocateAll: if resources > 0: # Have to allocate remaining resources return set([ActionSet([Action({'subject': self.name, 'verb': self.verbName, 'object': targets[0], 'amount': resources})])]) else: # Nothing left to allocate return set([ActionSet()]) else: target = targets[0] # If we don't consider this target, what other actions can we do? actions = self.getCombos(targets[1:],resources) # If we do consider this target, what other actions can we do? for amount in range(resources): action = Action({'subject': self.name, 'verb': self.verbName, 'object': target, 'amount': amount+1}) remaining = self.getCombos(targets[1:],resources-amount-1) actions = actions.union({partial.union({action}) for partial in remaining}) return actions def sampleAction(self,vector,numTargets=0,minResources=0,joint={}): """ @param numTargets: maximum number of targets for allocation. 0 means no limit (default is 0) @param minResources: minimum number of resources to leave unallocated (default is 0) @param joint: current joint action with which to coordinate Generates a random (legal) action for this agent in the given world """ if isinstance(vector,VectorDistribution): assert len(vector) == 1,'Unable to sample actions in an uncertain world' vector = vector.domain()[0] # How many resources do I have to allocate? resources = vector[stateKey(self.name,self.resourceName)] if resources > minResources: resources -= minResources else: # Less than minimal number, let's not divide them up numTargets = 1 # All legal objects are possible targets targets = Distribution() for obj in self.legalObjects(vector): targets[obj] = float(vector[stateKey(obj,'occupants')]) # All targets chosen by teammates are more likely targets for action in joint.values(): for atom in action: targets[atom['object']] /= 2. targets.normalize() while len(targets) > numTargets: element = targets.sample() del targets[element] targets.normalize() targets = targets.domain() actions = set() for target in targets: if target == targets[-1]: # Last target gets all remaining resources if isinstance(resources,float): amount = int(resources+0.5) else: amount = resources else: amount = random.randint(0,resources) if amount > 0: action = Action({'subject': self.name, 'verb': self.verbName, 'object': target, 'amount': amount}) actions.add(action) resources -= amount return ActionSet(actions) def legalObjects(self,vector): if isinstance(vector,VectorDistribution): assert len(vector.domain()) == 1,'Unable to determine legal objects in an uncertain world' vector = vector.domain()[0] return [obj for obj in self.objects if self.objectLegality[obj][vector]] def singletonActions(self,vector): """ Generates all possible single-country invasions I can do in the given world """ resources = self.world.getValue(stateKey(self.name,self.resourceName),vector) return [ActionSet([Action({'subject': self.name, 'verb': self.verbName, 'object': target, 'amount': resources})]) for target in self.legalObjects(vector)] def __xml__(self): doc = Agent.__xml__(self) doc.documentElement.setAttribute('resource',self.resourceName) doc.documentElement.setAttribute('verb',self.verbName) for obj in self.objects: node = doc.createElement('object') node.appendChild(doc.createTextNode(obj)) doc.documentElement.appendChild(node) for obj,tree in self.objectLegality.items(): node = doc.createElement('objectlegal') node.setAttribute('object',obj) node.appendChild(tree.__xml__().documentElement) doc.documentElement.appendChild(node) return doc def parse(self,element): Agent.parse(self,element) self.resourceName = str(element.getAttribute('resource')) self.verbName = str(element.getAttribute('verb')) self.objects = [] self.objectLegality = {} node = element.firstChild while node: if node.nodeType == node.ELEMENT_NODE: if node.tagName == 'object': self.objects.append(str(node.firstChild.data).strip()) elif node.tagName == 'objectlegal': obj = str(node.getAttribute('object')) subnode = node.firstChild while subnode: if subnode.nodeType == subnode.ELEMENT_NODE: tree = KeyedTree(subnode) self.objectLegality[obj] = tree break subnode = subnode.nextSibling node = node.nextSibling @staticmethod def isXML(element): if not Agent.isXML(element): return False return len(element.getAttribute('resource')) > 0 def closeRegions(regions): """ Makes the links symmetric in the given region map @type regions: strS{->}set(str) """ for orig,table in regions.items(): for dest in table['neighbors']: if not regions.has_key(dest): regions[dest] = {'neighbors': set(), 'value': 4} regions[dest]['neighbors'].add(orig) return regions def powerSet(limit): """ @return: a list of all possible combinations of numbers in the range of 1 to the given limit @rtype: set """ old = [[]] for i in range(limit): new = [] for partial in old: new.append(partial+[i+1]) new.append(partial) old = new return old def createWorld(numPlayers,regionTable,starts,generation='additive',maxResources=32,incentive='value'): """ @param numPlayers: number of players in the game @type numPlayers: int @param regionTable: a table of regions, indexed by name @param starts: a list of starting regions, one for each player @param maxResources: the maximum number of resources a player may have @type maxResources: int @param incentive: the type of reward for the agents (default is "value" for all) - B{value}: reward for cumulative value of individually owned territories - B{territory}: reward for number of individually owned territories - B{team}: reward for number of team owned territories @type: str (applied to all) or str[] (individually specified reward) """ if isinstance(incentive,str): incentive = [incentive for player in range(numPlayers)] world = ResourceWorld(allocateVerb='allocate',allocationState='invaders',winnerState='invader') # Create regions regions = set() for name,table in regionTable.items(): region = Agent(name) world.addAgent(region) regions.add(region) world.defineState(name,'occupants',int,lo=0,hi=maxResources, description='Number of resources in %s' % (region)) region.setState('occupants',table['occupants'] if table.has_key('occupants') else table['value']) world.defineState(name,'value',int,lo=0,hi=maxResources, description='Number of resources generated by %s' % (region)) region.setState('value',table['value']) world.defineState(name,'invaders',int,lo=0,hi=numPlayers*maxResources, description='Number of resources invading %s' % (region)) region.setState('invaders',0) world.dynamics[stateKey(region.name,'invaders')] = True # Create agents for human players players = [] for player in range(numPlayers): players.append(ResourceAgent('Player%d' % (player+1),'resources','allocate', [region.name for region in regions])) world.addAgent(players[player]) players[player].allocateAll = True world.defineState(players[player].name,'resources',int,lo=0,hi=maxResources, description='Number of total resources owned by %s' % (players[player].name), combinator='*') players[player].setState('resources',0) world.defineState(players[player].name,'territory',int,lo=0,hi=len(regionTable), combinator='*', description='Number of territories owned by %s' % (players[player].name)) players[player].setState('territory',0) world.dynamics[stateKey(players[player].name,'territory')] = True world.defineState(players[player].name,'value',int,lo=0, hi=sum([region['value'] for region in regionTable.values()]), combinator='*', description='Total vaue of territories owned by %s' % (players[player].name)) players[player].setState('value',0) world.dynamics[stateKey(players[player].name,'value')] = True # Reward functions for player in range(numPlayers): if incentive[player] == 'team': for other in range(numPlayers): players[player].setReward(maximizeFeature(stateKey(players[other].name,'territory')),1.) elif incentive[player] == 'territory': players[player].setReward(maximizeFeature(stateKey(players[player].name,'territory')),1.) else: # Assume "value" players[player].setReward(maximizeFeature(stateKey(players[player].name,'value')),1.) players[player].setAttribute('rationality',10.) players[player].setAttribute('discount',-10.) # Create agent for "enemy" enemy = Agent('Enemy') world.addAgent(enemy) owners = world.agents.keys() for region in regions: world.defineState(region.name,'owner',set,set(owners), description='Name of owner of %s' % (region)) region.setState('owner',enemy.name) world.defineState(region.name,'invader',set,set(owners)-{enemy.name}|{world.nullAgent}, description='Name of invader who will own %s if successful' % (region)) # try: # index = starts.index(region.name) # region.setState('invader','Player%d' % (index+1)) # except ValueError: region.setState('invader',world.nullAgent) world.dynamics[stateKey(region.name,'invader')] = True # Set players' initial territories world.objects.sort(lambda x,y: cmp(x.name,y.name)) for index in range(numPlayers): region = world.agents[starts[index]] region.setState('owner',players[index].name) world.objects.remove(region) world.objects.append(region) # Players can invade only if enemy owns it and they (or teammate) own a neighboring country for region in regions: tree = False for neighbor in regionTable[region.name]['neighbors']: tree = {'if': equalRow(stateKey(neighbor,'owner'),enemy.name), True: tree, False: True} tree = makeTree({'if': equalRow(stateKey(region.name,'owner'),enemy.name), True: tree, False: False}) players[index].objectLegality[region.name] = tree.desymbolize(world.symbols) # Create region "action" for region in regions: region.addAction({'verb': 'generate'}) # Set order of play world.setOrder([set([region.name for region in world.objects]),set([player.name for player in players])]) # Winner determination for region in regions: # Determine the owner after determining who's invading world.addDependency(stateKey(region.name,'owner'),stateKey(region.name,'invader')) # Determine the winner of the invasion owner = stateKey(region.name,'owner') world.dynamics[owner] = True invader = stateKey(region.name,'invader') defenders = stateKey(region.name,'occupants') invaders = stateKey(region.name,'invaders') value = stateKey(region.name,'value') for player in players: resources = stateKey(player.name,'resources') world.dynamics[resources] = True # # Determine how many resources lost # action = Action({'subject': player.name,'verb': 'allocate','object': region.name}) # # world.addDependency(resources,invader) # if generation == 'additive': # or generation == 'none': # # Lose only those resources allocated # tree = makeTree(incrementMatrix(resources,'-%s' % (actionKey('amount')))) # else: # # Lose all resources # tree = makeTree(setToConstantMatrix(resources,0)) # # world.setDynamics(resources,action,tree) # # Regain resources from owned territories # action = Action({'subject': region.name,'verb': 'generate'}) # if generation == 'additive' or generation == 'restorative': # tree = makeTree({'if': equalRow(owner,player.name), # True: addFeatureMatrix(resources,value), # False: None}) # elif generation == 'minimal': # if region is world.agents[starts[int(player.name[-1])-1]]: # # Get resources from home base (repeated) # tree = makeTree({'if': equalRow(owner,player.name), # True: addFeatureMatrix(resources,value), # False: None}) # else: # # And any new winnings (one-time) # tree = makeTree({'if': equalRow(owner,player.name), # True: {'if': equalFeatureRow(owner,invader), # True: addFeatureMatrix(resources,value), # False: None}, # False: None}) # elif generation is 'none': # if region is world.agents[starts[int(player.name[-1])-1]]: # # Get resources from home base if below threshold # tree = makeTree(addFeatureMatrix(resources,value)) # # tree = makeTree({'if': greaterThanRow(resources,value), # # True: None, # # False: setToFeatureMatrix(resources,value)}) # else: # tree = makeTree({'if': equalRow(owner,player.name), # True: {'if': equalFeatureRow(owner,invader), # True: addFeatureMatrix(resources,value), # False: None}, # False: None}) # world.setDynamics(resources,action,tree) # The game has two phases: generating resources and allocating resources world.defineState(None,'phase',list,['generate','allocate'],combinator='*', description='The current phase of the game') world.setState(None,'phase','generate') key = stateKey(None,'phase') world.dynamics[key] = True # Game ends when territory is all won tree = {'if': equalRow(key,'allocate'), True: True, False: False} for region in regions: tree = {'if': equalRow(stateKey(region.name,'owner'),enemy.name), True: False, False: tree} world.addTermination(makeTree(tree)) # Or if nobody has any resources vector = KeyedVector() for player in players: vector[stateKey(player.name,'resources')] = 1. tree = {'if': equalRow(stateKey(None,'phase'),'allocate'), True: {'if': KeyedPlane(vector,0.5), True: False, False: True}, False: False} world.addTermination(makeTree(tree)) # Keep track of which round it is world.defineState(None,'round',int,description='The current round of the game') world.setState(None,'round',0) action = Action({'subject': list(regions)[0].name, 'verb': 'generate'}) key = stateKey(None,'round') world.setDynamics(key,action,makeTree(incrementMatrix(key,1))) return world def readLogs(world,root): ignore = {'getPlayerRegionOwnershipInfo','getPrediction','getCommitCount', 'updateCommitFlagToMinusOne','getCompletedSurveyCount','inGameSurvey', 'getActionsForCurrentTurn','logMessage','updateCommitFlagToNull'} game = {'users': [],'states': [], 'moves': []} timeline = [] game['timeline'] = timeline for player in range(1,5): with open('%s_%d_logs.csv' % (root,player),'r') as csvfile: turn = 0 reader = csv.DictReader(csvfile) for row in reader: msg = row['spl_message'] elements = msg.split('|') assert len(elements) > 1 now = datetime.strptime(elements[0],'%Y-%m-%d %H:%M:%S.%f') if elements[1] in ignore: continue elif elements[1] == 'joinGame': entry = int(elements[2].split()[2]) game['users'].append(entry) elif elements[1] == 'preGameSurvey': entry = None elif elements[1] == 'pollStatus': if not 'PlayerRegionOwnershipInfo' in elements[2]: continue if player == 1: game['states'].append({}) game['moves'].append({i: {} for i in range(1,5)}) if not elements[2].split()[-1] in ['wait','=']: state = {'resources': {}, 'territories': {},'ownership': {}} for element in elements[3:7]: field = element.split(':') try: state['resources'][int(field[0][-1])] = int(field[1]) except ValueError: state['resources'][int(field[0][-1])] = int(field[1].split()[0]) for element in elements[7:49]: field = element.split(',') territory = field[0] if field[3][:5] == 'Enemy': owner = 0 else: owner = int(field[3][6]) state['territories'][territory] = owner if not state['ownership'].has_key(owner): state['ownership'][owner] = [] state['ownership'][owner].append(territory) if turn >= len(game['states']): print 'Extra turn %d for player %d' % (turn,player) else: if game['states'][turn]: if game['states'][turn] != state: print 'Discrepancy for player %d on turn %d' % (player,turn) else: game['states'][turn].update(state) turn += 1 entry = turn elif elements[1] == 'actionTaken': subelements = elements[2].split() if subelements[-1] == 'armies': territory = ' '.join(subelements[3:-3]) resources = int(subelements[-2]) entry = '%s,%d' % (territory,resources) try: game['moves'][turn-1][player][territory] = resources except IndexError: print 'Illegal move %d for player %d' % (turn,player) else: continue elif elements[1] == 'commitTurn': entry = None else: raise ValueError,'Unknown message type: %s' % (elements[1]) timeline.append((now,player,elements[1],entry)) timeline.sort() return game def analyzeGame(world,game,gameID): regions = sorted(game['states'][0]['territories'].keys()) fields = ['gameID','turn','content']+regions flow = [] collaborations = 0 solos = 0 moves = 0 outcomes = {'wins': 0, 'losses': 0} favorables = {'wins': 0,'losses': 0} coinflips = {'wins': 0,'losses': 0} unfavorables = {'wins': 0,'losses': 0} for index in range(len(game['moves'])): state = game['states'][index] record = {'gameID': gameID, 'turn': index, 'content': 'ownership', } for region in regions: record[region] = state['territories'][region] if index > 0: # Check what the outcome of previous moves was for region,prob in flow[-1]['win'].items(): pct = round(100.*prob) if record[region] == 0: # Loss outcomes['losses'] += 1 if pct > 50: favorables['losses'] += 1 elif pct == 50: coinflips['losses'] += 1 else: unfavorables['losses'] += 1 else: # Win outcomes['wins'] += 1 if pct > 50: favorables['wins'] += 1 elif pct == 50: coinflips['wins'] += 1 else: unfavorables['wins'] += 1 flow.append(record) record = {'gameID': gameID, 'turn': index+1, 'content': 'allocation', 'win': {}} for region in regions: record[region] = [] invaders = 0 leader = 0 for player,allocation in game['moves'][index].items(): try: record[region].append('%d' % (allocation[region])) if allocation[region] > 0: invaders += allocation[region] leader = max(leader,allocation[region]) moves += 1 except KeyError: record[region].append('0') assert len(record[region]) == 4 if record[region].count('0') == 3: solos += 1 else: collaborations += 4-record[region].count('0') record[region] = '|'.join(record[region]) if invaders > 0: # Compute win probability defenders = world.state[None].domain()[0][stateKey(region,'occupants')] record['win'][region] = float(invaders)/float(invaders+defenders) flow.append(record) print '%2d Turns' % (len(game['moves'])-1) print '%2d/42 Territories Won' % (len([region for region in regions if game['states'][-1]['territories'][region] != 0])) print '%2d/%2d Moves are Collaborative' % (collaborations,moves) print '%2d/%2d Invasions Won' % (outcomes['wins'],sum(outcomes.values())) print '%2d/%2d Favorable invasions won' % (favorables['wins'],sum(favorables.values())) print '%2d/%2d 50-50 invasions won' % (coinflips['wins'],sum(coinflips.values())) print '%2d/%2d Unfavorable invasions won' % (unfavorables['wins'],sum(unfavorables.values())) return fields,flow[:-1] def mapSave(regions,filename): """ Saves a region map to an XML file """ root = ET.Element('map') for name,table in regions.items(): node = ET.SubElement(root,'region') node.set('name',name) if table.has_key('value'): node.set('value',str(table['value'])) if table.has_key('occupants'): node.set('occupants',str(table['occupants'])) node.set('owner',str(table['owner'])) for neighbor in table['neighbors']: subnode = ET.SubElement(node,'neighbor') subnode.set('name',neighbor) tree = ET.ElementTree(root) tree.write(filename,pretty_print=True) return tree def mapLoad(filename,close=False): """ Parses an XML file representing a region map @param close: if C{True}, then fill in missing links and regions (default is False) @type close: bool """ tree = ET.parse(filename) regions = {} starts = [] for node in tree.getroot().getchildren(): assert node.tag == 'region' name = str(node.get('name')) assert not regions.has_key(name),'Duplicate region name: %s' % (name) regions[name] = {'value': int(node.get('value','5')), 'owner': int(node.get('owner','0')), 'occupants': int(node.get('occupants','5')), 'neighbors': set()} for subnode in node.getchildren(): assert subnode.tag == 'neighbor' regions[name]['neighbors'].add(str(subnode.get('name'))) if regions[name]['owner'] > 0: starts.append((regions[name]['owner'],name)) starts = [entry[1] for entry in sorted(starts)] if close: closeRegions(regions) return regions,starts def createAsia(): # Set of borders in Asia in Risk (only one direction included) asia = {'Afghanistan': {'neighbors': {'Ural','China','India','Middle East'}, 'value': 4,'occupants': 6}, 'China': {'neighbors': {'India','Ural','Siberia','Mongolia','Siam'}, 'value': 8,'occupants': 16}, 'India': {'neighbors': {'Middle East','Siam'}, 'value': 6,'occupants': 12}, 'Irkutsk': {'neighbors': {'Siberia','Yakutsk','Kamchatka','Mongolia'}, 'value': 4}, 'Japan': {'neighbors': {'Kamchatka','Mongolia'}, 'value': 4, 'occupants': 10}, 'Kamchatka': {'neighbors': {'Yakutsk','Mongolia'}, 'value': 4}, 'Mongolia': {'neighbors': {'Siberia'}, 'value': 4}, 'Siberia': {'neighbors': {'Ural','Yakutsk'}, 'value': 4} } # Fills out the transitive closure so that all neighbor links are bi-directional closeRegions(asia) starts = ['Ural','Middle East','Kamchatka','Siam'] for region,table in asia.items(): try: table['owner'] = starts.index(region)+1 except ValueError: table['owner'] = 0 mapSave(asia,'asia.xml') return asia def counts2incentives(counts): """ @return: a list of incentive specs that matches the given counts """ return ['value' for i in range(counts['value'])] + \ ['territory' for i in range(counts['individual'])] + \ ['team' for i in range(counts['team'])] if __name__ == '__main__': ###### # Parse command-line arguments ###### parser = ArgumentParser() # Positional argument that loads a map file parser.add_argument('map',help='XML file containing world map') # Optional argument that prints out predictions as well parser.add_argument('-p','--predict',action='store_true', help='print out predictions before stepping [default: %(default)s]') # Optional argument that sets the initial number of games to play parser.add_argument('-n','--number',action='store', type=int,default=1, help='Number of games to play [default: %(default)s]') # Optional argument that stops execution after 1 round parser.add_argument('-1','--single',action='store_true', help='stop execution after one round [default: %(default)s]') # Optional argument that specifies the name of the scenario file parser.add_argument('-f','--file',help='name of scenario file [default: <map root>.psy]') # Optional argument that specifies the root of the log file to replay parser.add_argument('-r','--replay',help='root of log files') # Optional argument that specifies the name of the log file parser.add_argument('-l','--log',help='name of log file [default: <map root>.csv]') # Optional argument that indicates game execution should lead to update of scenario file parser.add_argument('-u','--update',action='store_true',default=False, help='update scenario file [default: %(default)s]') # Optional argument that suppresses all output parser.add_argument('-q','--quiet',action='store_true', help='suppress all output [default: %(default)s]') # Optional arguments that determine the agent selection mode label = parser.add_argument_group('Decision Mode') group = label.add_mutually_exclusive_group() group.add_argument('-m','--manual',action='store_true', help='enter actions manually') group.add_argument('-a','--auto',action='store_false', dest='manual', help='agents choose actions autonomously [default]') # Optional arguments that determine the agent selection mode label = parser.add_argument_group('Agent Rewards') label.add_argument('-v','--value',type=int,default=0, help='Value of individually owned territories') label.add_argument('-i','--individual',type=int,default=0, help='Number of individually owned territories') label.add_argument('-t','--team',type=int,default=0, help='Number of team owned territories') # Optional arguments that select the resource generation model label = parser.add_argument_group('Resource Generation') group = label.add_mutually_exclusive_group() group.add_argument('--additive',action='store_const',const='additive',dest='generation', help='Resources from all territories, unused ones kept') group.add_argument('--restorative',action='store_const',const='restorative',dest='generation', help='Resources from all territories, unused ones lost') group.add_argument('--minimal',action='store_const',const='minimal',dest='generation', help='Resources from initial territories, unused ones lost') group.add_argument('--none',action='store_const',const='none',dest='generation', help='Resources from winning only, unused ones kept [default]') parser.set_defaults(generation='none',manual=False) args = vars(parser.parse_args()) if args['update']: assert args['number'] == 1,'Unable to update scenario file based on multiple games' if args['single']: assert args['number'] == 1,'Unable to perform single rounds based on multiple games' assert args['value'] + args['individual'] + args['team'] <= 4,\ 'Number of agent rewards exceeds number of players' args['value'] = 4 - args['individual'] - args['team'] incentives = counts2incentives(args) print incentives ###### # Set up map and world ###### regions,starts = mapLoad(args['map']) # closeRegions(regions) # mapSave(regions,args['map']) if args['file'] is None: args['file'] = '%s.psy' % (os.path.splitext(args['map'])[0]) if args['log'] is None: args['log'] = '%s.csv' % (os.path.splitext(args['map'])[0]) if os.path.isfile(args['file']): # Existing scenario file startTime = time.time() world = ResourceWorld(args['file']) if not args['quiet']: print >> sys.stderr,'Load:\t\t%3dms' % (1000.*(time.time()-startTime)) if world.terminated(): raise RuntimeError,'Game already over in scenario file %s' % (args['file']) else: world = createWorld(len(starts),regions,starts,args['generation'],incentive=incentives) world.save(args['file']) # world.printState() if args['replay']: if args['replay'] == 'all': files = set(os.listdir('.')) possibles = [fname[:4] for fname in files if fname[4:] == '_1_logs.csv'] games = [] for game in possibles: if not '%s.csv' % (game) in files: # No pre-existing merged log file for player in range(2,5): if not '%s_%d_logs.csv' % (game,player) in files: # Missing a player log break else: games.append(game) games.sort() else: games = [args['replay']] gameLogs = {} for gameID in games: print 'Game: %s' % (gameID) game = readLogs(world,gameID) if game['states']: fields,flow = analyzeGame(world,game,args['replay']) with open('%s.csv' % (args['replay']),'w') as csvfile: writer = csv.DictWriter(csvfile,fields,extrasaction='ignore') writer.writeheader() for record in flow: writer.writerow(record) sys.exit(0) # Set up end-of-game stat storage stats = {'rounds': Distribution(), # How many rounds did it take to win? 'win': Distribution({True: 0.,False: 0.})} # How often did the team win? for player in world.allocators: stats[player.name] = {'resources': Distribution(), # How many resources does the player end with? 'territory': Distribution(), # How many regions does the player end with? 'value': Distribution()} # What is the total value of those regions? for region in world.objects: stats[player.name][region.name] = Distribution() # Does the player end with this region? totalProb = 0. prediction = {} for iteration in range(args['number']): world = ResourceWorld(args['file']) if world.getValue('phase') == 'allocate': start = world.getValue('round') - 1 else: start = world.getValue('round') ###### # Game loop ###### # The probability of this current run probability = 1. while True: phase = world.getValue('phase') rnd = world.getValue('round') if phase == 'allocate': if not args['quiet']: # Print current game state print '--------' print 'Round %2d' % (rnd) print '--------' resources = world.getResources() regions = world.getOwnership() values = world.getTotalValue('value') for player in range(4): playerName = 'Player%d' % (player+1) print 'Player %d: %d resources' % (player+1,resources[playerName]) print '\t%2d territories (value: %3d): %s' % \ (world.getValue(stateKey(playerName,'territory')), world.getValue(stateKey(playerName,'value')), ', '.join(sorted(regions[playerName]))) total = 0 for region in regions['Player%d' % (player+1)]: total += world.getValue(stateKey(region,'value')) if regions.has_key('Enemy'): print 'Enemy: %s' % (', '.join(['%s (%d)' % (o,world.getValue(stateKey(o,'occupants'))) for o in sorted(regions['Enemy'])])) print # Check whether game is over if world.terminated(): break # Who's doing what turns = world.next() if phase == 'generate': actions = [] else: actions = {} # Initialize other players to be doing nothing others = {} for name in turns: actions[name] = ActionSet() others[name] = ActionSet() if args['manual']: turns.sort() for name in turns: if phase == 'generate': # Time for re-generation of resources assert not isinstance(world.agents[name],ResourceAgent) if name in starts: actions.append(Action({'subject': name,'verb': 'generate'})) else: actions.insert(0,Action({'subject': name,'verb': 'generate'})) else: assert phase == 'allocate' # Time for players to allocate resources agent = world.agents[name] if args['manual']: # Manual selection of actions objects = agent.legalObjects(world.state[None]) objects.sort() resources = world.getValue(stateKey(agent.name,agent.resourceName)) choices = set() while True: # Pick a target print for i in range(len(objects)): print '%2d) %s\t(value: %2d, defenders: %2d)' % \ (i+1,objects[i],world.getValue(stateKey(objects[i],'value')), world.getValue(stateKey(objects[i],'occupants'))) print ' 0) End %s\'s turn' % (name) print '-1) End game' print print 'Choose target for %s: ' % (name), try: index = int(sys.stdin.readline().strip()) except: continue if index == 0: # Chosen done break elif index == -1: sys.exit() if index > len(objects) or index < 0: # Illegal value continue # Pick an amount print '\nChoose resources for %s to allocate to %s (1-%d): ' \ % (agent.name,objects[index-1],resources), try: amount = int(sys.stdin.readline().strip()) except: continue if amount < 1 or amount > resources: # Illegal value continue print action = Action({'subject': agent.name, 'verb': agent.verbName, 'object': objects[index-1], 'amount': amount}) # Update available targets and resources del objects[index-1] resources -= amount choices.add(action) if resources == 0: # Nothing left to allocate break actions[name] = ActionSet(choices) else: startTime = time.time() currentWorld = world.state[None].domain()[0] choices = agent.singletonActions(currentWorld) choices.sort(lambda x,y: cmp(str(x),str(y))) keys = {} for action in choices: keys[action] = {stateKey(None,'phase'), stateKey(agent.name,'resources'), stateKey(agent.name,'territory'), stateKey(agent.name,'value'), stateKey(action['object'],'invader'), stateKey(action['object'],'owner')} decision = agent.decide(currentWorld,horizon=2,others=actions, selection='uniform',actions=choices,keys=keys) if len(choices) > 1: if not args['quiet']: print agent.name,', '.join(['%s %d (%5.3f)' % (a['object'],a['amount'],decision['V'][a]['__EV__']) for a in sorted(choices,lambda x,y: -cmp(decision['V'][x]['__EV__'],decision['V'][y]['__EV__']))]) if isinstance(decision['action'],Distribution): actions[name] = decision['action'].sample() else: actions[name] = decision['action'] if not args['quiet']: print >> sys.stderr,'Decision:\t%3dms (%s)' % \ (1000*(time.time()-startTime),actions[name]) # actions[name] = agent.sampleAction(world.state[None],1,joint=actions) if phase == 'allocate' and not args['quiet']: for player in range(4): print 'Player %d invades: %s' % (player+1,', '.join(['%s (%d)' % (a['object'],a['amount']) for a in actions['Player%d' % (player+1)]])) # Predict at possible outcomes if args['predict'] and phase == 'allocate' and not args['quiet']: startTime = time.time() prediction = world.predictResult(actions) if not args['quiet']: print >> sys.stderr,'Prediction:\t%3dms' % (1000.*(time.time()-startTime)) objects = prediction.keys() objects.sort() print print 'Predictions:' for obj in objects: print '\t%s (worth %d)' % (obj,world.getValue(stateKey(obj,'value'))) print '\t\tLeader:',','.join(prediction[obj]['leader'].domain()) print '\t\tWin: %d%%' % (100-int(100*prediction[obj]['winner']['Enemy'])) # Perform actions startTime = time.time() outcomes = world.step(actions,select=False) if not args['quiet']: print >> sys.stderr,'Step:\t\t%3dms' % (1000.*(time.time()-startTime)) if len(world.state[None]) > 1: original = VectorDistribution(world.state[None]) sample = world.state[None].select(True) sampleProb = original[world.state[None].domain()[0]] probability *= sampleProb if not args['quiet']: if not args['predict']: # Haven't figured out the objects yet objects = set() for name,action in actions.items(): for atom in action: objects.add(atom['object']) objects = list(objects) objects.sort() print print 'Results (prob %d%%):' % (int(100*sampleProb)) for obj in objects: key = stateKey(obj,'owner') owner = world.getValue(key) if owner == 'Enemy': print '%-10s:\tLost\t\t\tSpinner =%3d%%' % (obj,int(100.*sample[key])) else: print '%-10s:\tWon by %s\t+%d\tSpinner =%3d%%' % \ (obj,owner,world.getValue(stateKey(obj,'value')),int(100.*sample[key])) if phase == 'generate': if args['single'] and rnd == start+1: # Finished one round break # Accumulate end-of-game stats resources = world.getResources() regions = world.getOwnership() entry = {'probability': probability,'valueRewards': args['value'], 'territoryRewards': args['individual'],'teamRewards': args['team']} if regions.has_key('Enemy'): # Team lost stats['win'][False] += probability entry['win'] = 'no' else: # Team won stats['win'][True] += probability stats['rounds'].addProb(world.getValue('round'),probability) entry['win'] = 'yes' entry['rounds'] = world.getValue('round') for player in world.allocators: stats[player.name]['resources'].addProb(resources[player.name],probability) entry['resources%s' % (player.name)] = resources[player.name] territory = len(regions[player.name]) assert world.getValue(stateKey(player.name,'territory')) == territory stats[player.name]['territory'].addProb(territory,probability) entry['territory%s' % (player.name)] = territory value = sum([world.getValue(stateKey(region,'value')) for region in regions[player.name]]) assert world.getValue(stateKey(player.name,'value')) == value stats[player.name]['value'].addProb(value,probability) entry['value%s' % (player.name)] = value for region in world.objects: owned = world.getValue(stateKey(region.name,'owner')) == player.name stats[player.name][region.name].addProb(owned,probability) totalProb += probability with open(args['log'],'a') as csvfile: fieldnames = ['probability','win','rounds']+\ ['resources%s' % (p.name) for p in world.allocators]+\ ['territory%s' % (p.name) for p in world.allocators]+\ ['value%s' % (p.name) for p in world.allocators]+\ ['valueRewards','territoryRewards','teamRewards'] writer = csv.DictWriter(csvfile,fieldnames=fieldnames) writer.writerow(entry) if args['number'] > 1: # Normalize end-of-game stats stats['win'].normalize() stats['rounds'].normalize() for player in world.allocators: stats[player.name]['resources'].normalize() stats[player.name]['territory'].normalize() stats[player.name]['value'].normalize() for region in world.objects: stats[player.name][region.name].normalize() # Print end-of-game stats print 'Games:',args['number'] print 'Win: %3d%%' % (int(100.*stats['win'][True])) if stats['rounds']: print 'Rounds until win:' rounds = stats['rounds'].domain() rounds.sort() for r in range(rounds[0],rounds[-1]+1): print '\t%2d rounds: %2d%%' % (r,int(100.*stats['rounds'].getProb(r))) print world.allocators = sorted(world.allocators,key=lambda a: a.name) print 'Player\t\t%s' % ('\t'.join([player.name[-1] for player in world.allocators])) print 'E[resources]\t%s' % ('\t'.join(['%3d' % (stats[player.name]['resources'].expectation()) \ for player in world.allocators])) print 'E[regions]\t%s' % ('\t'.join(['%3d' % (stats[player.name]['territory'].expectation()) \ for player in world.allocators])) print 'E[value]\t%s' % ('\t'.join(['%3d' % (stats[player.name]['value'].expectation()) \ for player in world.allocators])) for region in sorted(world.objects,key=lambda a: a.name): if not region.name in starts: print '%-12s\t%s' % (region.name,'\t'.join(['%3d%%' % (int(100.*stats[player.name][region.name].getProb(True))) for player in world.allocators])) if args['update']: world.save(args['file'])