コード例 #1
0
ファイル: battleships.py プロジェクト: OBdA/battleships
class Player(object):

	# The init functions set all member of this class. It takes two
	# optional arguments: 'ki' is set to True if this is the computer
	# player, 'level' to define it's strength.
	def __init__(self, ki=False, level=50):

		# Asking for 'human' I prefer before asking for dump, deadly
		# fast calculating machines...so, I called the member 'human'.
		# I called the "level" now 'ki_level' to make clear it is only
		# valid for computer players, not for human ones.
		self.human		= not ki
		self.ki_level	= level

		# The Player needs some counters for counting his ships
		# 'ship_count' which not sunk so far and a list of ships his foe
		# has already. This is 'foeships'.
		self.ship_count	= 0
		self.foeships	= []

		# Now the maps. Because the handling of maps may be difficult,
		# this is encapsulated in another class called 'Map'. I called
		# them 'ships' for the secret map the Player hold his own ships.
		# And 'hits' which is his open map to track the bombardments.
		self.ships	= Map()
		self.hits	= Map()

		# The player (especially the KI) needs to remember the last
		# turn's result. So here we hold space to save is...
		self.last_result = None


		## public methods


	# To play "battleships" you have to set up the game first. Some of
	# this is done automatically while initialize the object (like
	# create space and initializing counters). The placement of the
	# ships should not be done automatically (but it is done yet in
	# __main__).
	# The following function places _one_ ship onto the secret map.
	# Therefor it gets itself (the Player object) and a ship definition,
	# containing the type and size of the ship.
	#
	def place_ship(self, shipdef):
		"""
		Randomly set a ship onto ship map and returns the ship region or
		None if no space is left.
		"""
		assert isinstance(shipdef, dict), "'shipdef' must be of type 'dict'"

		# Get the type and the size of the ship from the ship
		# definition.
		what, size = shipdef['name'], shipdef['size']

		# Just get the map in a shorthand form.
		mymap = self.ships

		# I calculate a list with all free regions with a minimum size
		# of the ship's size, this is done with 'mymap.regions(size)'.
		# Then I choose a region randomly from this list an return None
		# to the caller if there is no space left.
		region = RAND.choice(mymap.regions(size))
		if len(region) == 0: return None

		# The returned region has a _minimum_ size and can be greater
		# then the ship's size. I choose now the part of the region we
		# use for the ship. I get the starting point first.
		# This can be one of 'size - lenght-of-region' fields.
		first = RAND.randint(0,len(region)-size)

		# Now I set 'size' fields (beginning with the first field,
		# choosen above) to the 'ship' status.
		mymap.set_fields(region[first:first+size], 'ship')

		# Thats important!
		# Because the ships are not allowed to be connected, I set all
		# surrounding fields to 'water'. These fields not empty anymore,
		# so they will not be choosen to place a ship again.
		mymap.set_fields(
			mymap.neighbours(set(region[first:first+size])),
			'water'
		)

		# count this ship and return the region used
		self.ship_count += 1
		return region[first:first+size]


	def cleanup_ships_map(self):
		"""
		Cleanup the ships map of all the helpful water fields.
		"""
		mymap = self.ships
		mymap.set_fields(mymap.get_fields('water'), None)


	def save_foes_ships(self, shipdef):
		# I have to copy this dictionary deeply, not copying the
		# reference to it only.
		self.foeships = copy.deepcopy(shipdef)


	def is_all_sunk(self):
		"""
		Returns True when all own ships are sunk.
		"""
		if self.ship_count < 1: return True
		return False


	def turn(self):
		"""
		Play one turn and return a koordninate to bomb the foe's ships.
		Returns None when when player resigns or has no possible turn left.
		"""
		if self.human:
			# This is the human part of turn() -- I do not comment it
			# and left it to the reader...
			if self.last_result:
				lkoor,lstat = self.last_result
				if lstat == 'sunk':
					self.hits.surround_with(lkoor, 'water')

				elif lstat == 'hit':
					self._mark_hit_ship(lkoor)

			bomb_map = self.hits
			ship_map = self.ships

			koor = None		# is a tuple()
			while True:
				line = input( "\nCaptain> " )
				token = line.rstrip("\n").split()
				token.append('')	# empty lines fail to pop()
				cmd = token.pop(0).lower()
				if re.match('^(resign|aufgeben|quit|exit|ende|stop)', cmd):
					exit(0)
				elif re.match('^(hilfe|help)', cmd):
					_print_help()
				elif re.match('^(skip)', cmd):
					break
				elif cmd == '':
					bomb_map.print()
				elif cmd == 'ships':
					ship_map.print()
				elif cmd == 'strategie':
					t_map = self._best_moves()
					Map(t_map).print()
				elif cmd == 'tmap':
					t_map = self._rate_unknown_fields(int(token[0]))
					Map(t_map).print()
				elif cmd == 'tipp':
					t_map = self._best_moves()
#					Map(t_map).print()
					best_rate = max(t_map.values())
					best_moves= [k for k,v in t_map.items() if v == best_rate]
					print('Mmmm..vieleicht auf {}'.format(as_xy(RAND.choice(best_moves))))
				elif re.match('[a-z]\d+', cmd):
					koor = as_koor(cmd)
					if koor == None:
						print( "-- Gib ein Feld bitte mit einem Buchstaben und " \
							"einer Zahl ein.\n-- Zum Beispiel: {0}{1}"\
							.format(RAND.choice(X_SET),RAND.choice(Y_SET)) )
						continue
					elif bomb_map.get(koor) != None:
						feld = bomb_map.get(koor)
						print( "-- Oh, Captain!")
						print( "-- Im Feld {0} ist doch schon '{1}'".format(
							X_SET[koor[0]] + str(Y_SET[koor[1]]),
							feld
						))
						continue
					break
				else:
					print( "-- Häh? Versuche es mal mit 'hilfe'.")
			return koor

		# This is the KI part.
		# I create the 'target_map' with a set of the best moves. On the
		# following lines I will add some more fields with calculated
		# 'move rates' which helps to find the best move.
		#
		# _best_moves() tries to find the field with the highest
		# possibility to find a foe's ship.
		target_map = self._best_moves()

		# get the highest rate from the target map
		best_rate  = max(target_map.values())

		# ...and get the best moves out of the target map
		best_moves = [xy for xy,val in target_map.items() if val == best_rate]

		# Now, get one of the best moves and return it!
		f =  RAND.choice(best_moves)
		##print('foes turn: {} with {} points'.format(f, target_map[f]))
		return f


	def bomb(self, koor):
		"""
		Bomb a field on the ship map and return the result.
		"""
		assert isinstance(koor, tuple), "Need a tuple as field coordinate."

		result = None
		mymap = self.ships

		# get the status of the bombed field
		status = mymap.get(koor)
		if status == 'ship' or status == 'hit':
			# We are hit!
			mymap.set(koor, 'hit')

			# check wheter our ship was completly sunk
			# get all neighbouring 'ship' or 'hit' fields
			ship = mymap.neighbours(
				{koor}, status={'ship', 'hit'}, include=True, recursive=True
			)
			# get all 'hit' fields only
			hits = mymap.neighbours(
				{koor}, status='hit', include=True, recursive=True
			)
#			print('SHIP:',ship,'HITS:',hits)

			# compare the number of fields in the sets, if there more
			# hits than ship fields this ship must be sunk
			if len(ship-hits) < 1:
				mymap.set_fields(ship, 'sunk')
				self.ship_count -= 1
				result =  (koor, 'sunk')
			else:
				result =  (koor, 'hit')

		# Oh yeah, only water was hit
		elif status == None or status == 'water':
			mymap.set(koor, 'water')
			result =  (koor, 'water')

		# If something gets wrong we raise an exception
		if result == None:
			raise Exception("sync error in protocol", (koor, status))

		# send a message and return
		self.send_message('foe_has_' + result[1], result)
		return result


	def handle_result(self, result):
		"""
		Handle all the possible results of a bombardment
		"""
		assert isinstance(result, tuple), "<result> must be an tuple (koor,status)"

		# we got the result, consisting of a coordinate and a result
		koor,status = result
		assert status in STATUS_SET, "no valid <status> in <result>"
		mymap = self.hits

		# Great! We sunk a ship!
		if status == 'sunk':
			mymap.set(koor, status)
			ship = mymap.neighbours( {koor}, {'hit','sunk'}, True, True)

			# mark sunken ship in my mymap
			mymap.set_fields(ship, 'sunk')

			# find the sunken ship in the list of foe's ships and count
			# it down -- save the name for the following message
			name = None
			for s in self.foeships:
				if s['size'] == len(ship):
					s['num'] -= 1
					name = s['name']
					break
			if name == None:
				raise Exception("Not existing ship sunk", [result,ship,len(ship)])

			# send the message which ship was sunk
			self.send_message('result_sunk', name, ship)

		# Good, we hit a ship.
		elif status == 'hit':
			mymap.set(koor, status)
			self.send_message( 'result_' + status, result )

		# Oh, nothing noticeable hit.
		elif status == 'water':
			mymap.set(koor, status)
			self.send_message( 'result_' + status, result )

		# Damn! There must be something wrong...
		else:
			raise Exception("unable to handle result", result)

		# Save the last result for later use and return
		self.last_result = result
		return


	# This function does the communication part with the players. All
	# possible messages are listed here. If you want to localize you
	# only have to do this in this function.
	# FIXME: do localize all messages
	def send_message(self, msgid, *args):
		"""
		Send message to the player (only for interactive mode).
		"""
		if not self.human: return

		# Perhaps, we should ask the player for his name and get it from
		# the object here.
		#FIXME: ask the player for his name in init code and use it here
		name = 'Kapitän'
		if msgid == 'ships_distributed':
			print("{}! Es wurden {} Schiffe verteilt.".format(
				name, args[0]
			))

		elif msgid == 'result_sunk':
			print("{}! Wir haben ein {} versenkt!".format(
				name, args[0]
			))

		elif msgid == 'result_hit':
			print("{}! Wir haben auf Feld {} ein Schiff getroffen!".format(
				name, as_xy(args[0][0])
			))

		elif msgid == 'result_water':
			print("{}! Wasser.".format(
				name, args[0]
			))

		elif msgid == 'foe_has_sunk':
			#FIXME: add type of the sunken ship
			print("{}! Unser Gegner hat unser Schiff bei {} versenkt!".format(
				name, as_xy(args[0][0])
			))

		elif msgid == 'foe_has_hit':
			#FIXME: add type of the hit ship
			print("{}! Unser Gegner hat unser Schiff bei {} getroffen!".format(
				name, as_xy(args[0][0])
			))

		elif msgid == 'foe_has_water':
			print("{}! Unser Gegner macht Wellen bei {}.".format(
				name, as_xy(args[0][0])
			))

		elif msgid == 'you_win':
			print("{}! DU HAST GEWONNEN!".format(name))

		elif msgid == 'you_lost':
			print("{}! DU HAST LEIDER VERLOREN!".format(name))

		else:
			print("UNKNOWN MESSAGE:", msgid, '>>', args)

		return


	def _mark_hit_ship(self, field):
		"""
		Mark the fields around a hit field which are 'diagonal', because
		there can not be another ship.
		"""
		assert isinstance(field, tuple), "field must be a tuple"
		assert field[0] in range(len(X_SET)) and field[1] in range(len(Y_SET)),\
			"field must be element of (X_SET, Y_SET)"

		# find all 'diagonal' fields and mark them as water
		self.hits.set_fields(
			self.hits.neighbours({field}, check='odd'),
			'water'
		)

		return


	# Oh yeah -- the magical function which calculates the best moves.
	# This is the one which give you a hint or let the computer move.
	def _best_moves(self):
		"""
		Calculate the best move and return the coordinates.
		"""

		# I use a level for this KI, ranging from 0 to 100. If a human
		# player ask fo a hint use the maximum value to get the best
		# result.
		level = self.ki_level
		if self.human: level = 100

		# I create a map with a rate for each field. The field with the
		# highest rate will be bombed...
		rate_map = dict()

		# I use the result from the last turn to calculate some
		# additional information, eg. to mark a hit field or a sunken
		# ship. All thios is done with a certain possibility only.
		if self.last_result != None:
			field, status = self.last_result

			# to mark the 'diagonal' fields of a hit field is hard. (I
			# got it after thinking a lot myself.)
			if status == 'hit' and RAND.randint(0,100) <= level + LEVEL['hard']:
				self._mark_hit_ship(field)
				#print('level:',level,'mark_hit_ship_at:',field)

			# mark a sunken ship my son got easily -- so it should easy
			# too.
			if status == 'sunk' and RAND.randint(0,100) <= level + LEVEL['easy']:
				self.hits.surround_with(field, 'water')
				#print('level:',level,'ship_is_sunk')

			# got a rate for unknown fields i give the 'intermediate'
			# level. My son got the idea fast (to bomb the big hole at
			# the map), but to find the right field on the big holes are
			# quite difficulty. 
			if RAND.randint(0,100) <= level + LEVEL['intermediate']:
				# what is the maximum ship size we are searching for?
				maximum = max(
					[shipdef['size'] for shipdef in self.foeships \
					if shipdef['num'] > 0]
				)
				# get the rate map for the unknown fields and add them
				# to our target map (rate_map)
				rmap = self._rate_unknown_fields(size=maximum)
				for f in rmap.keys():
					rate_map[f] = rate_map.get(f,0) + rmap[f]
				#print('level:',level,'rate_fields_size:',maximum)

			# suppose we hit a ship on a turn formerly. This code trys
			# to sunk all this ships we find.
			hits = self.hits.get_fields('hit')
			if len(hits) > 0 and RAND.randint(0,100) <= level + LEVEL['easy']:
				#print('level:', level, 'destroy_ship:',hits, end=' ')

				# get one field, get the ship and find all empty neighbours
				field = hits.pop()
				fields = self.hits.neighbours(
					self.hits.get_region(field),
					status=None
				)

				# add rate to all this empty fields possibly containing
				# the remaining ship -- use a static value of 20.
				for f in fields:
					rate_map[f] = rate_map.get(f,0) + 20

		# This is a fall-back.
		# If we are unlucky and get no target map this will rate all
		# empty field with 1. At a result an empty field will be bombed
		# randomly.
		if self.last_result == None or len(rate_map) < 1:
			rate_map = {koor:1 for koor in self.hits.get_fields(None)}
			#print('LEVEL', level, 'random_field')
		#print('LEVEL', level, 'RATED MAP IS:')
		#Map(rate_map).print()

		return rate_map


	def _rate_unknown_fields(self, size=1, rate=1):
		"""
		Rate unknown fields of the open map and return a <target map>.
		The Parameter 'size' is the minimal size of a region (default: 1). 
		Bewertet unbekannte Felder der Map und gibt eine <target map>
		zurück. The parameter 'rate' is the basis to calculate the 
		<target map> (default: 1).
		"""

		# initialize the <target map> as an dictionary
		t_map = dict()

		# get all regions with the minimum of 'size'
		regions = self.hits.regions(size)

		# now get all regions found and calculate a value list for each
		# region. the calculated values are merged with the <target map>.
		# The function calc_points() calculates for each field of a region a
		# value, depending on the distance from the center of the region.
		# fields in the center have the highest value, fields at the edge
		# the lowest.
		for region in regions:
			val_list = calc_points(region, rate)
			for k,v in val_list.items():
				t_map[k] = t_map.get(k, 0) + v

		return t_map