-
Notifications
You must be signed in to change notification settings - Fork 33
/
Copy pathbusters.py
565 lines (465 loc) · 20.8 KB
/
busters.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# busters.py
# ----------
# Licensing Information: Please do not distribute or publish solutions to this
# project. You are free to use and extend these projects for educational
# purposes. The Pacman AI projects were developed at UC Berkeley, primarily by
# John DeNero ([email protected]) and Dan Klein ([email protected]).
# Student side autograding was added by Brad Miller, Nick Hay, and Pieter
# Abbeel in Spring 2013.
# For more info, see http://inst.eecs.berkeley.edu/~cs188/pacman/pacman.html
"""
Busters.py is a vengeful variant of Pacman where Pacman hunts ghosts, but
cannot see them. Numbers at the bottom of the display are noisy distance
readings to each remaining ghost.
To play your first game, type 'python pacman.py' from the command line.
The keys are 'a', 's', 'd', and 'w' to move (or arrow keys). Have fun!
"""
from game import GameStateData
from game import Game
from game import Directions
from game import Actions
from game import Configuration
from util import nearestPoint
from util import manhattanDistance
import sys, util, types, time, random, layout, os
########################################
# Parameters for noisy sensor readings #
########################################
SONAR_NOISE_RANGE = 15 # Must be odd
SONAR_MAX = (SONAR_NOISE_RANGE - 1)/2
SONAR_NOISE_VALUES = [i - SONAR_MAX for i in range(SONAR_NOISE_RANGE)]
SONAR_DENOMINATOR = 2 ** SONAR_MAX + 2 ** (SONAR_MAX + 1) - 2.0
SONAR_NOISE_PROBS = [2 ** (SONAR_MAX-abs(v)) / SONAR_DENOMINATOR for v in SONAR_NOISE_VALUES]
def getNoisyDistance(pos1, pos2):
if pos2[1] == 1: return None
distance = util.manhattanDistance(pos1, pos2)
return max(0, distance + util.sample(SONAR_NOISE_PROBS, SONAR_NOISE_VALUES))
observationDistributions = {}
def getObservationDistribution(noisyDistance):
"""
Returns the factor P( noisyDistance | TrueDistances ), the likelihood of the provided noisyDistance
conditioned upon all the possible true distances that could have generated it.
"""
global observationDistributions
if noisyDistance == None:
return util.Counter()
if noisyDistance not in observationDistributions:
distribution = util.Counter()
for error , prob in zip(SONAR_NOISE_VALUES, SONAR_NOISE_PROBS):
distribution[max(1, noisyDistance - error)] += prob
observationDistributions[noisyDistance] = distribution
return observationDistributions[noisyDistance]
###################################################
# YOUR INTERFACE TO THE PACMAN WORLD: A GameState #
###################################################
class GameState:
"""
A GameState specifies the full game state, including the food, capsules,
agent configurations and score changes.
GameStates are used by the Game object to capture the actual state of the game and
can be used by agents to reason about the game.
Much of the information in a GameState is stored in a GameStateData object. We
strongly suggest that you access that data via the accessor methods below rather
than referring to the GameStateData object directly.
Note that in classic Pacman, Pacman is always agent 0.
"""
####################################################
# Accessor methods: use these to access state data #
####################################################
def getLegalActions( self, agentIndex=0 ):
"""
Returns the legal actions for the agent specified.
"""
if self.isWin() or self.isLose(): return []
if agentIndex == 0: # Pacman is moving
return PacmanRules.getLegalActions( self )
else:
return GhostRules.getLegalActions( self, agentIndex )
def generateSuccessor( self, agentIndex, action):
"""
Returns the successor state after the specified agent takes the action.
"""
# Check that successors exist
if self.isWin() or self.isLose(): raise Exception('Can\'t generate a successor of a terminal state.')
# Copy current state
state = GameState(self)
# Let agent's logic deal with its action's effects on the board
if agentIndex == 0: # Pacman is moving
state.data._eaten = [False for i in range(state.getNumAgents())]
PacmanRules.applyAction( state, action )
else: # A ghost is moving
GhostRules.applyAction( state, action, agentIndex )
# Time passes
if agentIndex == 0:
state.data.scoreChange += -TIME_PENALTY # Penalty for waiting around
else:
GhostRules.decrementTimer( state.data.agentStates[agentIndex] )
# Resolve multi-agent effects
GhostRules.checkDeath( state, agentIndex )
# Book keeping
state.data._agentMoved = agentIndex
state.data.score += state.data.scoreChange
p = state.getPacmanPosition()
state.data.ghostDistances = [getNoisyDistance(p, state.getGhostPosition(i)) for i in range(1,state.getNumAgents())]
if agentIndex == self.getNumAgents() - 1:
state.numMoves += 1
return state
def getLegalPacmanActions( self ):
return self.getLegalActions( 0 )
def generatePacmanSuccessor( self, action ):
"""
Generates the successor state after the specified pacman move
"""
return self.generateSuccessor( 0, action )
def getPacmanState( self ):
"""
Returns an AgentState object for pacman (in game.py)
state.pos gives the current position
state.direction gives the travel vector
"""
return self.data.agentStates[0].copy()
def getPacmanPosition( self ):
return self.data.agentStates[0].getPosition()
def getNumAgents( self ):
return len( self.data.agentStates )
def getScore( self ):
return self.data.score
def getCapsules(self):
"""
Returns a list of positions (x,y) of the remaining capsules.
"""
return self.data.capsules
def getNumFood( self ):
return self.data.food.count()
def getFood(self):
"""
Returns a Grid of boolean food indicator variables.
Grids can be accessed via list notation, so to check
if there is food at (x,y), just call
currentFood = state.getFood()
if currentFood[x][y] == True: ...
"""
return self.data.food
def getWalls(self):
"""
Returns a Grid of boolean wall indicator variables.
Grids can be accessed via list notation, so to check
if there is food at (x,y), just call
walls = state.getWalls()
if walls[x][y] == True: ...
"""
return self.data.layout.walls
def hasFood(self, x, y):
return self.data.food[x][y]
def hasWall(self, x, y):
return self.data.layout.walls[x][y]
##############################
# Additions for Busters Pacman #
##############################
def getLivingGhosts(self):
"""
Returns a list of booleans indicating which ghosts are not yet captured.
The first entry (a placeholder for Pacman's index) is always False.
"""
return self.livingGhosts
def setGhostNotLiving(self, index):
self.livingGhosts[index] = False
def isLose( self ):
return self.maxMoves > 0 and self.numMoves >= self.maxMoves
def isWin( self ):
return self.livingGhosts.count(True) == 0
def getNoisyGhostDistances(self):
"""
Returns a noisy distance to each ghost.
"""
return self.data.ghostDistances
#############################################
# Helper methods: #
# You shouldn't need to call these directly #
#############################################
def __init__( self, prevState = None ):
"""
Generates a new state by copying information from its predecessor.
"""
if prevState != None:
self.data = GameStateData(prevState.data)
self.livingGhosts = prevState.livingGhosts[:]
self.numMoves = prevState.numMoves;
self.maxMoves = prevState.maxMoves;
else: # Initial state
self.data = GameStateData()
self.numMoves = 0;
self.maxMoves = -1;
self.data.ghostDistances = []
def deepCopy( self ):
state = GameState( self )
state.data = self.data.deepCopy()
state.data.ghostDistances = self.data.ghostDistances
return state
def __eq__( self, other ):
"""
Allows two states to be compared.
"""
return self.data == other.data
def __hash__( self ):
"""
Allows states to be keys of dictionaries.
"""
return hash( str( self ) )
def __str__( self ):
return str(self.data)
def initialize( self, layout, numGhostAgents=1000 ):
"""
Creates an initial game state from a layout array (see layout.py).
"""
self.data.initialize(layout, numGhostAgents)
self.livingGhosts = [False] + [True for i in range(numGhostAgents)]
self.data.ghostDistances = [getNoisyDistance(self.getPacmanPosition(), self.getGhostPosition(i)) for i in range(1, self.getNumAgents())]
def getGhostPosition( self, agentIndex ):
if agentIndex == 0:
raise "Pacman's index passed to getGhostPosition"
return self.data.agentStates[agentIndex].getPosition()
def getGhostState( self, agentIndex ):
if agentIndex == 0:
raise "Pacman's index passed to getGhostPosition"
return self.data.agentStates[agentIndex]
############################################################################
# THE HIDDEN SECRETS OF PACMAN #
# #
# You shouldn't need to look through the code in this section of the file. #
############################################################################
COLLISION_TOLERANCE = 0.7 # How close ghosts must be to Pacman to kill
TIME_PENALTY = 1 # Number of points lost each round
class BustersGameRules:
"""
These game rules manage the control flow of a game, deciding when
and how the game starts and ends.
"""
def newGame( self, layout, pacmanAgent, ghostAgents, display, maxMoves= -1 ):
agents = [pacmanAgent] + ghostAgents
initState = GameState()
initState.initialize( layout, len(ghostAgents))
game = Game(agents, display, self)
game.state = initState
game.state.maxMoves = maxMoves
return game
def process(self, state, game):
"""
Checks to see whether it is time to end the game.
"""
if state.isWin(): self.win(state, game)
if state.isLose(): self.lose(state, game)
def win( self, state, game ):
game.gameOver = True
def lose( self, state, game ):
game.gameOver = True
class PacmanRules:
"""
These functions govern how pacman interacts with his environment under
the classic game rules.
"""
def getLegalActions( state ):
"""
Returns a list of possible actions.
"""
return Actions.getPossibleActions( state.getPacmanState().configuration, state.data.layout.walls )
getLegalActions = staticmethod( getLegalActions )
def applyAction( state, action ):
"""
Edits the state to reflect the results of the action.
"""
legal = PacmanRules.getLegalActions( state )
if action not in legal:
raise "Illegal action", action
pacmanState = state.data.agentStates[0]
# Update Configuration
vector = Actions.directionToVector( action, 1)
pacmanState.configuration = pacmanState.configuration.generateSuccessor( vector )
applyAction = staticmethod( applyAction )
class GhostRules:
"""
These functions dictate how ghosts interact with their environment.
"""
def getLegalActions( state, ghostIndex ):
conf = state.getGhostState( ghostIndex ).configuration
return Actions.getPossibleActions( conf, state.data.layout.walls )
getLegalActions = staticmethod( getLegalActions )
def applyAction( state, action, ghostIndex):
legal = GhostRules.getLegalActions( state, ghostIndex )
if action not in legal:
raise Exception("Illegal ghost action: " + str(action))
ghostState = state.data.agentStates[ghostIndex]
vector = Actions.directionToVector( action, 1 )
ghostState.configuration = ghostState.configuration.generateSuccessor( vector )
applyAction = staticmethod( applyAction )
def decrementTimer( ghostState):
timer = ghostState.scaredTimer
if timer == 1:
ghostState.configuration.pos = nearestPoint( ghostState.configuration.pos )
ghostState.scaredTimer = max( 0, timer - 1 )
decrementTimer = staticmethod( decrementTimer )
def checkDeath( state, agentIndex):
pacmanPosition = state.getPacmanPosition()
if agentIndex == 0: # Pacman just moved; Anyone can kill him
for index in range( 1, len( state.data.agentStates ) ):
ghostState = state.data.agentStates[index]
ghostPosition = ghostState.configuration.getPosition()
if GhostRules.canKill( pacmanPosition, ghostPosition ):
GhostRules.collide( state, ghostState, index )
else:
ghostState = state.data.agentStates[agentIndex]
ghostPosition = ghostState.configuration.getPosition()
if GhostRules.canKill( pacmanPosition, ghostPosition ):
GhostRules.collide( state, ghostState, agentIndex )
checkDeath = staticmethod( checkDeath )
def collide( state, ghostState, agentIndex):
state.data.scoreChange += 200
GhostRules.placeGhost(ghostState, agentIndex)
# Added for first-person
state.data._eaten[agentIndex] = True
state.setGhostNotLiving(agentIndex)
collide = staticmethod( collide )
def canKill( pacmanPosition, ghostPosition ):
return manhattanDistance( ghostPosition, pacmanPosition ) <= COLLISION_TOLERANCE
canKill = staticmethod( canKill )
def placeGhost(ghostState, agentIndex):
pos = (agentIndex * 2 - 1, 1)
direction = Directions.STOP
ghostState.configuration = Configuration(pos, direction)
placeGhost = staticmethod( placeGhost )
class RandomGhost:
def __init__( self, index ):
self.index = index
def getAction( self, state ):
return random.choice( state.getLegalActions( self.index ) )
def getDistribution( self, state ):
actions = state.getLegalActions( self.index )
prob = 1.0 / len( actions )
return [( prob, action ) for action in actions]
#############################
# FRAMEWORK TO START A GAME #
#############################
def default(str):
return str + ' [Default: %default]'
def parseAgentArgs(str):
if str == None: return {}
pieces = str.split(',')
opts = {}
for p in pieces:
if '=' in p:
key, val = p.split('=')
else:
key,val = p, 1
opts[key] = val
return opts
def readCommand( argv ):
"""
Processes the command used to run pacman from the command line.
"""
from optparse import OptionParser
usageStr = """
USAGE: python busters.py <options>
EXAMPLE: python busters.py --layout bigHunt
- starts an interactive game on a big board
"""
parser = OptionParser(usageStr)
parser.add_option('-n', '--numGames', dest='numGames', type='int',
help=default('the number of GAMES to play'), metavar='GAMES', default=1)
parser.add_option('-l', '--layout', dest='layout',
help=default('the LAYOUT_FILE from which to load the map layout'),
metavar='LAYOUT_FILE', default='oneHunt')
parser.add_option('-p', '--pacman', dest='pacman',
help=default('the agent TYPE in the pacmanAgents module to use'),
metavar='TYPE', default='BustersKeyboardAgent')
parser.add_option('-a','--agentArgs',dest='agentArgs',
help='Comma seperated values sent to agent. e.g. "opt1=val1,opt2,opt3=val3"')
parser.add_option('-g', '--ghosts', dest='ghost',
help=default('the ghost agent TYPE in the ghostAgents module to use'),
metavar = 'TYPE', default='RandomGhost')
parser.add_option('-q', '--quietTextGraphics', action='store_true', dest='quietGraphics',
help='Generate minimal output and no graphics', default=False)
parser.add_option('-k', '--numghosts', type='int', dest='numGhosts',
help=default('The maximum number of ghosts to use'), default=4)
parser.add_option('-z', '--zoom', type='float', dest='zoom',
help=default('Zoom the size of the graphics window'), default=1.0)
parser.add_option('-f', '--fixRandomSeed', action='store_true', dest='fixRandomSeed',
help='Fixes the random seed to always play the same game', default=False)
parser.add_option('-s', '--showGhosts', action='store_true', dest='showGhosts',
help='Renders the ghosts in the display (cheating)', default=False)
parser.add_option('-t', '--frameTime', dest='frameTime', type='float',
help=default('Time to delay between frames; <0 means keyboard'), default=0.1)
options, otherjunk = parser.parse_args()
if len(otherjunk) != 0:
raise Exception('Command line input not understood: ' + otherjunk)
args = dict()
# Fix the random seed
if options.fixRandomSeed: random.seed('bustersPacman')
# Choose a layout
args['layout'] = layout.getLayout( options.layout )
if args['layout'] == None: raise Exception("The layout " + options.layout + " cannot be found")
# Choose a ghost agent
ghostType = loadAgent(options.ghost, options.quietGraphics)
args['ghosts'] = [ghostType( i+1 ) for i in range( options.numGhosts )]
# Choose a Pacman agent
noKeyboard = options.quietGraphics
pacmanType = loadAgent(options.pacman, noKeyboard)
agentOpts = parseAgentArgs(options.agentArgs)
agentOpts['ghostAgents'] = args['ghosts']
pacman = pacmanType(**agentOpts) # Instantiate Pacman with agentArgs
args['pacman'] = pacman
import graphicsDisplay
args['display'] = graphicsDisplay.FirstPersonPacmanGraphics(options.zoom, \
options.showGhosts, \
frameTime = options.frameTime)
args['numGames'] = options.numGames
return args
def loadAgent(pacman, nographics):
# Looks through all pythonPath Directories for the right module,
pythonPathStr = os.path.expandvars("$PYTHONPATH")
if pythonPathStr.find(';') == -1:
pythonPathDirs = pythonPathStr.split(':')
else:
pythonPathDirs = pythonPathStr.split(';')
pythonPathDirs.append('.')
for moduleDir in pythonPathDirs:
if not os.path.isdir(moduleDir): continue
moduleNames = [f for f in os.listdir(moduleDir) if f.endswith('gents.py')]
for modulename in moduleNames:
try:
module = __import__(modulename[:-3])
except ImportError:
continue
if pacman in dir(module):
if nographics and modulename == 'keyboardAgents.py':
raise Exception('Using the keyboard requires graphics (not text display)')
return getattr(module, pacman)
raise Exception('The agent ' + pacman + ' is not specified in any *Agents.py.')
def runGames( layout, pacman, ghosts, display, numGames, maxMoves=-1):
# Hack for agents writing to the display
import __main__
__main__.__dict__['_display'] = display
rules = BustersGameRules()
games = []
for i in range( numGames ):
game = rules.newGame( layout, pacman, ghosts, display, maxMoves )
game.run()
games.append(game)
if numGames > 1:
scores = [game.state.getScore() for game in games]
wins = [game.state.isWin() for game in games]
winRate = wins.count(True)/ float(len(wins))
print 'Average Score:', sum(scores) / float(len(scores))
print 'Scores: ', ', '.join([str(score) for score in scores])
print 'Win Rate: %d/%d (%.2f)' % (wins.count(True), len(wins), winRate)
print 'Record: ', ', '.join([ ['Loss', 'Win'][int(w)] for w in wins])
return games
if __name__ == '__main__':
"""
The main function called when pacman.py is run
from the command line:
> python pacman.py
See the usage string for more details.
> python pacman.py --help
"""
args = readCommand( sys.argv[1:] ) # Get game components based on input
runGames( **args )