''' Created on Jun 19, 2016 @author: Brett Paufler Copyright 6-19-16 This is a Logical (non-user-interface) implementation of Risk-it Based Rules for Risk-It Played on a 8x8 board with set starting positions At beginning of turn, +1 unit to each square controlled Player can move from square they occupy, up, down, left, or right Player must make a move every turn Win or lose, player loses all unit but one in attack stack If Win, player controls defending square with 1 unit If Win, losing player must check for surrender If Surrender, Attacking player essentially wins every square of defender Attack is d10 per unit on square So, randint(0, 9) for each unit on square If any zero, total is zero else sum of all rolls Tie goes to defender snapshot csv data is of the form: game_num, turn_num, player, units, squares, attacker, defender where units & squares are the players total units & squares and attacker, defender are current turn_num participants (not necessarily player) if save_image = True, saves a image representation of the units on the game board at the end of every turn Not Implemented: Skill Level proposed to do by taking a slice off the top of the sorted attacks_possible If Logic() class used in tk game_num, will need to pass in Unit Locations Current Turn. remaining players, etc Tournament Play Hook A Tournament plays multiple games A preset suite of tournaments might prove useful ''' import numpy as np from skimage import io from random import randint, choice class Attack(): '''Used to store each individual attack possibility. All possible attacks are computed for every player every turn. However, scoring is only done for advanced play.''' def __init__(self, row_att, col_att, row_def, col_def, defender, score=0): self.row_att = row_att self.col_att = col_att self.row_def = row_def self.col_def = col_def self.defender = defender self.score = score def __repr__(self): return self.__str__() def __str__(self): text = 'Attack: %d, From: (%d,%d), To: (%d, %d)' % ( self.score, self.row_att, self.col_att, self.row_def, self.col_def) return text class Logic(): '''Basic implementation of Risk It using image and csv output Random or strategic output''' def __init__(self, player_logic_list, save_images = False, ): '''player_logic_list: ['neutral', 'player', 'random', 'random', 'random'] First item in list is ingored but must be included random: random attack selection player: logical choice save images: True, saves images at end of each turn ''' self.save_images = save_images self.player_logic_list = (player_logic_list if player_logic_list else ['neutral', 'player', 'random', 'random', 'random']) #row, col, player: player 0: neutrals, players 1-4: colored squares self.units = np.zeros(shape=(8, 8, 5), dtype=np.int16) #header for snapshot preloaded self.data_snapshots = 'game_num,turn_num,player,units,squares,attacker,defender\n' self.game_num = 0 self.turn_num = 1 self.current_player = 1 self.players_remaining_in_game = [1, 2, 3, 4] #populated with all possible attacks each turn self.attacks_possible = [] #dummy attack, this is selected from attacks_possible each turn self.attack = Attack(-9, -9, -9, -9, 0, 0) #so numpy arrays are easier to read np.set_printoptions(formatter={'int':lambda x: '{: >2}'.format(x)}) def __str__(self): text = 'Game: %d, Turn: %d, Player: %d\n' % ( self.game_num, self.turn_num, self.current_player) for row in range(8): for player in range(5): text += '%s ' % (self.units[row, :, player]) text += '\n' return text def report_attack(self): '''Helpful feedback during/after an attack.''' text = 'Turn: %d: Attacker: %d, Defender: %s, %s' % ( self.turn_num, self.current_player, self.attack.defender, self.attack) return text def report_game_over(self): '''Runs at Game Termination''' winners = [str(player) for player in self.players_remaining_in_game] text = 'GAME: %d, Turn: %d, Winner(s) %s' % ( self.game_num, self.turn_num, ' '.join(winners)) return text #A Report for Tournament Play (as well as a tournament hook, would make sense) ########################################################################### # Helper Functions Used Throughout (not square specific, player specific) ########################################################################### def players_total_units(self, player): num_units = np.sum(self.units[:, :, player]) return num_units def players_total_squares(self, player): players_units = self.units[:, :, player] row_col_tuple = np.nonzero(players_units) number_squares = len(row_col_tuple[0]) return number_squares ########################################################################### # snapshots (csv of game_num state for graphs) ########################################################################### def add_snapshot_data(self): defender = self.attack.defender # if self.attack.defender # else None for player in self.players_remaining_in_game: self.data_snapshots += '%d,%d,%d,%d,%d,%d,%s\n' % ( self.game_num, self.turn_num, player, self.players_total_units(player), self.players_total_squares(player), self.current_player, defender) def save_data_snapshot_as_txt(self): with open('./output/risk_it_snapshot_data.txt', 'w') as f: f.write(self.data_snapshots) ########################################################################### # Image (save game_num state of units) ########################################################################### def save_img_units(self): img = np.zeros((500, 500, 3), dtype=np.int16) img = self.img_add_white_border(img) img = self.img_add_grid_lines(img) img = self.img_add_color_squares(img) save_name = './output/{:0>3}_{:0>3}_units.png'.format(self.game_num, self.turn_num) io.imsave(save_name, img) def img_add_white_border(self, img): img[0:15, :, :] = 255 #top img[485:500, :, :] = 255 #bottom img[:, 0:15, :] = 255 #left img[:, 485:500, :] = 255 #right return img def img_add_grid_lines(self, img): for line in range(1, 8): line_start = line*60 + 5 line_end = line_start + 10 img[line_start:line_end, :, :] = 255 #horizontal img[:, line_start:line_end, :] = 255 #vertical return img def img_add_color_squares(self, img): #RGB codes pink, cyan, yellow, green rgb_colors = [(255, 192, 203), (0, 255, 255), (255, 255, 0), (0, 255, 0)] for layer, color in enumerate(rgb_colors, start=1): for row in range(8): for col in range(8): if self.units[row, col, layer]: row_start = 15 + row * 60 row_end = row_start + 50 col_start = 15 + col * 60 col_end = col_start + 50 img[row_start:row_end, col_start:col_end, :] = color return img ########################################################################### # Tournaments (multi-game_num control) ########################################################################### def tournament(self, num_games): self.tournament_play(num_games) self.save_data_snapshot_as_txt() def tournament_play(self, num_games): for _ in range(num_games): self.play_game() self.game_num += 1 print 'Tournament Over' ########################################################################### # Game (start and Loop until one player remains) ########################################################################### def play_game(self): self.game_reset() while len(self.players_remaining_in_game) > 1: self.play_turn() print self.report_game_over() def game_reset(self): self.turn_num = 1 self.current_player = 1 self.players_remaining_in_game = range(1, 5) self.units_starting_positions() def units_starting_positions(self): self.units[:, :, :] = 0 self.units[:, :, 0] = 1 for row, col, player in [(2, 2, 1), (2, 5, 2), (5, 5, 3), (5, 2, 4)]: self.units[row, col, player] = 1 self.units[row, col, 0] = 0 ########################################################################### # Play Turn ########################################################################### def play_turn(self): self.turn_increment_units() self.attack_select() self.battle() self.save_game_state() self.turn_reset_and_advance() def turn_increment_units(self): players_units = self.units[:, :, self.current_player] players_units[players_units >= 1] += 1 self.units[:, :, self.current_player] = players_units def save_game_state(self): if self.save_images: self.save_img_units() def turn_reset_and_advance(self): self.attack = None self.attacks_possible = [] self.turn_num += 1 self.current_player = self.next_player(self.current_player) def next_player(self, player): '''Returns next player in self.players_still_in_game, starting at player and looping around to first at end of list.''' index = self.players_remaining_in_game.index(player) index += 1 if index == len(self.players_remaining_in_game): index = 0 next_player = self.players_remaining_in_game[index] return next_player ########################################################################### # Battle (includes taking over square and surrender) ########################################################################### def battle(self): roll_att = self.roll_dice(self.units[ self.attack.row_att, self.attack.col_att, self.current_player]) roll_def = self.roll_dice(self.units[ self.attack.row_def, self.attack.col_def, self.attack.defender]) if roll_att > roll_def: self.battle_attacker_wins_takeover_square() if self.surrender_condition(self.attack.defender): self.surrender_protocol() self.battle_resolution_attacker_units_to_one() def roll_dice(self, num): rolls = [randint(0, 9) for _ in range(num)] value = sum(rolls) if 0 not in rolls else 0 return value def surrender_condition(self, opponents_number): if opponents_number == 0: #non-player cannot surrender surrenders = False elif opponents_number == self.current_player: #cannot surrender to self surrenders = False else: units_attacker = self.players_total_units(self.current_player) units_defender = self.players_total_units(opponents_number) squares_attacker = self.players_total_squares(self.current_player) squares_defender = self.players_total_squares(opponents_number) players_remaining = len(self.players_remaining_in_game) significantly_more_units = (players_remaining * units_defender) <= units_attacker significantly_more_squares = (players_remaining * squares_defender) <= squares_attacker surrenders = significantly_more_units or significantly_more_squares return surrenders def surrender_protocol(self): defenders_squares = self.units[:, :, self.attack.defender] defenders_squares[defenders_squares >= 1] = 1 self.units[:, :, self.current_player] += defenders_squares self.units[:, :, self.attack.defender] = 0 self.players_remaining_in_game.remove(self.attack.defender) self.add_snapshot_data() def battle_resolution_attacker_units_to_one(self): self.units[self.attack.row_att, self.attack.col_att, self.current_player] = 1 def battle_attacker_wins_takeover_square(self): self.units[self.attack.row_def, self.attack.col_def, self.attack.defender] = 0 self.units[self.attack.row_def, self.attack.col_def, self.current_player] = 1 ########################################################################### # Attacks Possible (the logical attack decision, reason for this module) ########################################################################### def attack_select(self): self.attacks_possible = [] self.attacks_possible_update_scoreless() if self.player_logic_list[self.current_player] == 'player': self.attacks_possible_score_all() self.attacks_possible_sort() self.attack_choose_from_possible() def attacks_possible_update_scoreless(self): attackers_units_by_row_col = self.attackers_units_in_row_col_form() for row_att, col_att in attackers_units_by_row_col: for row_def, col_def in self.list_of_neighboring_squares(row_att, col_att): if (row_def, col_def) not in attackers_units_by_row_col: defender = self.player_from_square(row_def, col_def) self.attacks_possible.append( Attack(row_att, col_att, row_def, col_def, defender, score = 0)) def attackers_units_in_row_col_form(self): attackers_units_list_of_row_col_tuples = [(row, col) for row, col in list(np.transpose(np.nonzero(self.units[:, :, self.current_player])))] return attackers_units_list_of_row_col_tuples def list_of_neighboring_squares(self, row, col): '''Given a square index, returns valid surrounding indexes. 1, 1 yields [(1, 0), (1, 2), (0, 1), (2, 1)].''' neighbors = [(row, col) for row, col in (row +1, col), (row -1, col), (row, col +1), (row, col -1) if 0 <= row <= 7 and 0 <= col <= 7] return neighbors def player_from_square(self, row, col): '''Given a row, col, returns None or player_number.''' units_on_square = self.units[row, col] player_array = np.nonzero(units_on_square) player = int(list(player_array).pop()) return player def attacks_possible_score_all(self): defenders_static_scores = self.players_specific_score_list() for attack in self.attacks_possible: factor_attacker = self.roll_dice_average( self.units[attack.row_att, attack.col_att, self.current_player]) factor_defender = self.roll_dice_average( self.units[attack.row_def, attack.col_def, attack.defender]) static = defenders_static_scores[attack.defender] specific = factor_attacker - factor_defender - 2 #safety #The intent here was to devaule attacks with low liklihood of success #But there isn't much different in outcome from what had before if specific > 0: score = static * specific else: score = static + specific attack.score = score def players_specific_score_list(self): players_scores = [0] # Attacking a neutral square always scores at 0, always for player in range(1, 5): players_units = self.players_total_units(player) players_squares = self.players_total_squares(player) player_will_surrender = self.surrender_condition(player) players_sort_order = 5 * player score = 0 score += players_units score += - players_squares score += 25 * player_will_surrender score += players_sort_order players_scores.append(score) return players_scores def roll_dice_average(self, num): min_value = 1 value = min_value + int(num * 5 * pow(0.90, num)) if num else num return value def attacks_possible_sort(self): self.attacks_possible = sorted( self.attacks_possible, key=lambda attack: attack.score, reverse=True) def attack_choose_from_possible(self): if self.player_logic_list[self.current_player] == 'random': self.attack = choice(self.attacks_possible) elif self.player_logic_list[self.current_player] == 'player': self.attack = self.attacks_possible.pop(0) else: assert True == False if __name__ == '__main__': '''Runs a Tournament, saving to hard-wired directory './output/' save_image (True/False): whether to output images player_logic_list: five item list, first item is ignored random = random.choice(self.possible_attacks) player = best of the score ranked self.possible_attacks num_games: number of games in tournament 1 & 10 being good debug numbers 1000 for the presentation data ''' logic = Logic( save_images=False, player_logic_list=['neutral', 'player', 'random', 'random', 'random'] ) logic.tournament(num_games=1)