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