Advertisement
Guest User

Untitled

a guest
Oct 22nd, 2015
499
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 94.79 KB | None | 0 0
  1. import itertools
  2. import operator
  3. import random
  4. import time
  5. import pygame
  6. import math
  7. from collections import defaultdict
  8. class Connect3DError(Exception):
  9.     pass
  10.  
  11. BACKGROUND = (250, 250, 255)
  12. LIGHTBLUE = (86, 190, 255)
  13. LIGHTGREY = (200, 200, 200)
  14. GREY = (128, 128, 128)
  15. BLACK = (0, 0, 0)
  16. WHITE = (255, 255, 255)
  17. RED = (255, 0, 0)
  18. GREEN = (0, 255, 0)
  19. BLUE = (0, 0, 255)
  20. YELLOW = (255, 255, 0)
  21. CYAN = (0, 255, 255)
  22. MAGENTA = (255, 0, 255)
  23. SELECTION = {'Default': [WHITE, LIGHTGREY],
  24.              'Hover': [None, BLACK],
  25.              'Waiting': [None, BLACK],
  26.              'Selected': [GREEN, None]}
  27.  
  28. def mix_colour(*args):
  29.     mixed_colour = [0, 0, 0]
  30.     num_colours = len(args)
  31.     for colour in range(3):
  32.         mixed_colour[colour] = sum(i[colour] for i in args) / num_colours
  33.     return mixed_colour
  34.    
  35. class Connect3D(object):
  36.     """Class to store and use the Connect3D game data.
  37.    The data is stored in a 1D list, but is converted to a 3D representation for the user.
  38.    """
  39.     player_symbols = 'XO'
  40.     default_segments = 4
  41.     default_shuffle_count = 3
  42.     from_string_separator = '/'
  43.     bot_difficulty_default = 'medium'
  44.    
  45.     def __init__(self, segments=default_segments):
  46.         """Set up the grid and which player goes first.
  47.        
  48.        Parameters:
  49.            segments (int): How long each side of the grid should be.
  50.                The game works best with even numbers, 4 is recommended.
  51.        """
  52.        
  53.         #Set current player
  54.         try:
  55.             self.current_player
  56.         except AttributeError:
  57.             self.current_player = random.randint(0, 1)
  58.        
  59.         #Set up grid
  60.         try:
  61.             self.segments = int(segments)
  62.         except TypeError:
  63.             raise TypeError('segments must be an integer')
  64.  
  65.         self.ai_message = []
  66.  
  67.         self.play_data = []
  68.         self.segments_squared = pow(self.segments, 2)
  69.         self.segments_cubed = pow(self.segments, 3)
  70.         self.range_data = range(self.segments_cubed)
  71.         self.grid_data = ['' for i in self.range_data]
  72.         self.update_score()
  73.        
  74.         #Calculate the edge numbers for each direction
  75.         self.direction_edges = {}
  76.         self.direction_edges['U'] = range(self.segments_squared)
  77.         self.direction_edges['D'] = range(self.segments_squared*(self.segments-1), self.segments_squared*self.segments)
  78.         self.direction_edges['R'] = [i*self.segments+self.segments-1 for i in range(self.segments_squared)]
  79.         self.direction_edges['L'] = [i*self.segments for i in range(self.segments_squared)]
  80.         self.direction_edges['F'] = [i*self.segments_squared+j+self.segments_squared-self.segments for i in range(self.segments) for j in range(self.segments)]
  81.         self.direction_edges['B'] = [i*self.segments_squared+j for i in range(self.segments) for j in range(self.segments)]
  82.         self.direction_edges[' '] = []
  83.        
  84.         #Calculate the addition needed to move in each direction
  85.         self.direction_maths = {}
  86.         self.direction_maths['D'] = self.segments_squared
  87.         self.direction_maths['R'] = 1
  88.         self.direction_maths['F'] = self.segments
  89.         self.direction_maths['U'] = -self.direction_maths['D']
  90.         self.direction_maths['L'] = -self.direction_maths['R']
  91.         self.direction_maths['B'] = -self.direction_maths['F']
  92.         self.direction_maths[' '] = 0
  93.        
  94.        
  95.     def __repr__(self):
  96.         """Format the data to allow it to be imported again as a new object."""
  97.         grid_data_joined = ''.join(str(i).ljust(1) for i in self.grid_data)
  98.         longest_number = len(str(pow(self.segments, 3) - 1))
  99.        
  100.         repr_format = '{}{s}{}'.format(grid_data_joined, self.current_player, s=self.from_string_separator)
  101.        
  102.         if self.play_data is not None and self.range_data is not None:
  103.             play_data_joined = ''.join(str(i).zfill(longest_number) for i in self.play_data)
  104.             range_data_joined = ''.join(str(i).zfill(longest_number) for i in self.range_data)
  105.             repr_format += '{s}{}{s}{}'.format(play_data_joined, range_data_joined, s=self.from_string_separator)
  106.            
  107.            
  108.         return "Connect3D.from_string('{}')".format(repr_format)
  109.    
  110.     def __str__(self):
  111.         """Use the grid_data to output a grid of the correct size.
  112.        Each value in grid_data must be 1 character or formatting will be wrong.
  113.        
  114.        >>> grid_data = range(8)
  115.        
  116.        >>> print Connect3D.from_string(''.join(str(i) if i != '' else ' ' for i in grid_data))
  117.             ________
  118.            / 0 / 1 /|
  119.           /___/___/ |
  120.          / 2 / 3 /  |
  121.         /___/___/   |
  122.        |   |____|___|
  123.        |   / 4 /|5 /
  124.        |  /___/_|_/
  125.        | / 6 / 7|/
  126.        |/___/___|
  127.        """
  128.         k = 0
  129.        
  130.         grid_range = range(self.segments)
  131.         grid_output = []
  132.        
  133.         for j in grid_range:
  134.            
  135.             row_top = ' '*(self.segments*2+1) + '_'*(self.segments*4)
  136.             if j:
  137.                 row_top = '|' + row_top[:self.segments*2-1] + '|' + '_'*(self.segments*2) + '|' + '_'*(self.segments*2-1) + '|'
  138.             grid_output.append(row_top)
  139.            
  140.             for i in grid_range:
  141.                 row_display = ' '*(self.segments*2-i*2) + '/' + ''.join((' ' + str(self.grid_data[k+x]).ljust(1) + ' /') for x in grid_range)
  142.                 k += self.segments
  143.                 row_bottom = ' '*(self.segments*2-i*2-1) + '/' + '___/'*self.segments
  144.                
  145.                 if j != grid_range[-1]:
  146.                     row_display += ' '*(i*2) + '|'
  147.                     row_bottom += ' '*(i*2+1) + '|'
  148.                 if j:
  149.                     row_display = row_display[:self.segments*4+1] + '|' + row_display[self.segments*4+2:]
  150.                     row_bottom = row_bottom[:self.segments*4+1] + '|' + row_bottom[self.segments*4+2:]
  151.                    
  152.                     row_display = '|' + row_display[1:]
  153.                     row_bottom = '|' + row_bottom[1:]
  154.                
  155.                 grid_output += [row_display, row_bottom]
  156.                
  157.         return '\n'.join(grid_output)
  158.    
  159.     def _get_winning_player(self):
  160.         """Return a list of the player(s) with the highest points.
  161.        
  162.        >>> C3D = Connect3D()
  163.        >>> C3D.update_score()
  164.        
  165.        When X has a higher score.
  166.        >>> C3D.current_points['X'] = 5
  167.        >>> C3D.current_points['O'] = 1
  168.        >>> C3D._get_winning_player()
  169.        ['X']
  170.        
  171.        When both scores are the same.
  172.        >>> C3D.current_points['O'] = 5
  173.        >>> C3D._get_winning_player()
  174.        ['O', 'X']
  175.        
  176.        When there are no winners.
  177.        >>> C3D = Connect3D()
  178.        >>> C3D.update_score()
  179.        >>> C3D._get_winning_player()
  180.        []
  181.        """
  182.         self.update_score()
  183.         return get_max_dict_keys(self.current_points)
  184.    
  185.     @classmethod
  186.     def from_string(cls, raw_data):
  187.         """Create new Connect3D instance from a string.
  188.        
  189.        Parameters:
  190.            raw_data (str): Passed in from __repr__,
  191.                contains the grid data and current player.
  192.                Will still work if no player is defined.
  193.                Format: "joined(grid_data).current_player.joined(zfilled(play_data)).joined(zfilled(range_data))"
  194.        """
  195.         split_data = raw_data.split(cls.from_string_separator)
  196.         grid_data = [i if i != ' ' else '' for i in split_data[0]]
  197.         segments = calculate_segments(grid_data)
  198.         longest_number = len(str(pow(segments, 3) - 1))
  199.        
  200.         new_c3d_instance = cls(segments)
  201.        
  202.         new_c3d_instance.grid_data = grid_data
  203.         new_c3d_instance.play_data = None
  204.         new_c3d_instance.range_data = None
  205.        
  206.         #Get current player
  207.         if len(split_data) > 1:
  208.             new_c3d_instance.current_player = split_data[1]
  209.            
  210.         #Get range and play data
  211.         if len(split_data) > 3:
  212.             formatted_play_data = [int(split_data[2][j:j+longest_number]) for j in range(len(split_data[2]))[::longest_number]]
  213.             if all(grid_data[i] for i in formatted_play_data) and formatted_play_data == list(set(formatted_play_data)):
  214.                 new_c3d_instance.play_data = formatted_play_data
  215.                
  216.             formatted_range_data = [int(split_data[3][j:j+longest_number]) for j in range(len(split_data[3]))[::longest_number]]
  217.             if sorted(formatted_range_data) == range(pow(segments, 3)):
  218.                 new_c3d_instance.range_data = formatted_range_data
  219.        
  220.         new_c3d_instance.update_score()
  221.        
  222.         return new_c3d_instance
  223.        
  224.     @classmethod
  225.     def from_list(cls, grid_data, player=None, play_data=None, range_data=None):
  226.         """Create new Connect3D instance from lists.
  227.        
  228.        Parameters:
  229.            grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.
  230.            
  231.            player (int or None): Current player to continue the game with.
  232.            
  233.            play_data (list or None): List containing the ID of each move currently taken.
  234.                If range_data is None, this will be set to None.
  235.            
  236.            range_data (list or None): List containing the current position of original cell IDs.
  237.                If play_data is None, this will be set to None.
  238.        """
  239.         segments = calculate_segments(grid_data)
  240.         new_c3d_instance = cls(segments)
  241.        
  242.         new_c3d_instance.grid_data = [i if i != ' ' else '' for i in grid_data]
  243.        
  244.         if player is not None:
  245.             new_c3d_instance.current_player = player
  246.        
  247.         if play_data is not None and range_data is not None:
  248.             if not all(grid_data[i] for i in play_data) or not sorted(set(range_data)) == range(pow(segments, 3)):
  249.                 play_data = None
  250.                 range_data = None
  251.         new_c3d_instance.play_data = play_data
  252.         new_c3d_instance.range_data = range_data
  253.        
  254.         new_c3d_instance.update_score()
  255.        
  256.         return new_c3d_instance
  257.        
  258.     def _old_play(self, p1=False, p2=bot_difficulty_default, shuffle_after=default_shuffle_count, end_when_no_points_left=False):
  259.         """NEEDS TO BE UPDATED TO WORK
  260.        
  261.        Start or continue a game.
  262.        If using computer players, there is a minimum time delay to avoid it instantly making moves.
  263.        
  264.        Parameters:
  265.            player1 (bool): If player 1 should be played by a computer,
  266.                and if so, what difficulty level.
  267.            
  268.            player2 (bool): If player 2 should be played by a computer,
  269.                and if so, what difficulty level.
  270.        """
  271.        
  272.         players = (p1, p2)
  273.        
  274.         self.current_player = int(not self.current_player)
  275.         move_number = 1
  276.         shuffle_count = 0
  277.         shuffle_after = shuffle_after or self.default_shuffle_count
  278.         min_time_update = 0.65
  279.        
  280.         #Display score and grid
  281.         print
  282.         self.update_score()
  283.         print self.show_score()
  284.         print self
  285.        
  286.         #Game loop
  287.         while True:
  288.             print
  289.             print 'Move {}:'.format(move_number/2)
  290.             move_number += 1
  291.            
  292.             current_time = time.time()
  293.            
  294.             #Switch current player
  295.             self.current_player = int(not self.current_player)
  296.            
  297.             #Check if any points are left to gain
  298.             points_left = True
  299.             if end_when_no_points_left:
  300.                 potential_points = {self.player_symbols[j]: Connect3D.from_list([self.player_symbols[j] if i == '' else i for i in self.grid_data]).current_points for j in (0, 1)}
  301.                 if any(self.current_points == potential_points[player] for player in (self.player_symbols[j] for j in (0, 1))):
  302.                     points_left = False
  303.                
  304.             #Check if no spaces are left
  305.             if '' not in self.grid_data or not points_left:
  306.                 winning_player = self._get_winning_player()
  307.                 if len(winning_player) == 1:
  308.                     print 'Player {} won!'.format(winning_player[0])
  309.                 else:
  310.                     print 'The game was a draw!'
  311.                    
  312.                 #Ask to play again and check if answer is a variant of 'yes' or 'ok'
  313.                 print 'Play again?'
  314.                 play_again = raw_input().lower()
  315.                 if any(i in play_again for i in ('y', 'k')):
  316.                     self.reset()
  317.                 else:
  318.                     return
  319.                     break
  320.            
  321.            
  322.             print "Player {}'s turn...".format(self.player_symbols[self.current_player])
  323.             if (p1 == False and not self.current_player) or (p2 == False and self.current_player):
  324.                
  325.                 #Player takes a move, function returns True if it updates the grid, otherwise loop again
  326.                 while True:
  327.                     player_input = raw_input().replace(',', ' ').replace('.', ' ').split()
  328.                     player_go = self.make_move(self.player_symbols[self.current_player], player_input)
  329.                     if player_go is None:
  330.                         print "Grid cell is not available, try again."
  331.                     else:
  332.                         if self.play_data is not None:
  333.                             self.play_data.append(self.range_data[player_go])
  334.                         break
  335.             else:
  336.                 #AI takes a move, will stop the code if it does something wrong
  337.                 ai_go = SimpleC3DAI(self, self.current_player, players[self.current_player]).calculate_next_move()
  338.                 if self.make_move(self.player_symbols[self.current_player], ai_go) is None:
  339.                     raise Connect3DError('Something unknown went wrong with the AI')
  340.                 else:
  341.                     print "AI moved to point {}.".format(PointConversion(self.segments, ai_go).to_3d())
  342.                     if self.play_data is not None:
  343.                         self.play_data.append(self.range_data[ai_go])
  344.                    
  345.                 #Wait a short while
  346.                 time.sleep(max(0, min_time_update - (time.time() - current_time)))
  347.    
  348.            
  349.             shuffle_count += 1
  350.             if shuffle_after and shuffle_count >= shuffle_after:
  351.                 self.shuffle()
  352.                
  353.             #Display score and grid
  354.             self.update_score()
  355.             print self.show_score()
  356.             print self
  357.             if shuffle_after and shuffle_count >= shuffle_after:
  358.                 shuffle_count = 0
  359.                 print "Grid was flipped!"
  360.  
  361.     def play(self, p1=False, p2=bot_difficulty_default, allow_shuffle=True, end_when_no_points_left=False):
  362.         self = RunPygame(self).play(p1, p2, allow_shuffle, end_when_no_points_left)
  363.                
  364.     def make_move(self, id, *args):
  365.         """Update the grid data with a new move.
  366.        
  367.        Parameters:
  368.            id (str): Character to write into the grid.
  369.            
  370.            args (int, tuple or list): Where in the grid to place the ID.
  371.                Can be input as an integer (grid cell number), 3 integers,
  372.                a tuple or list (3D coordinates)
  373.        
  374.        Returns the ID moved to or None if no move was made.
  375.        
  376.        >>> C3D = Connect3D(2)
  377.        
  378.        >>> C3D.make_move('a', 1)
  379.        1
  380.        >>> C3D.make_move('b', 1)
  381.        >>> C3D.make_move('c', -1)
  382.        >>> C3D.make_move('d', 2, 2, 2)
  383.        7
  384.        >>> C3D.make_move('e', [1, 1, 2])
  385.        4
  386.        >>> C3D.make_move('f', (1, 1, 3))
  387.        
  388.        >>> C3D.grid_data
  389.        ['', 'a', '', '', 'e', '', '', 'd']
  390.        >>> print C3D
  391.             ________
  392.            /   / a /|
  393.           /___/___/ |
  394.          /   /   /  |
  395.         /___/___/   |
  396.        |   |____|___|
  397.        |   / e /|  /
  398.        |  /___/_|_/
  399.        | /   / d|/
  400.        |/___/___|
  401.        """
  402.        
  403.         #Convert points to the grid cell ID
  404.         if len(args) == 1:
  405.             if not str(args[0]).replace('-','').isdigit():
  406.                 if len(args[0]) == 1:
  407.                     try:
  408.                         i = int(args[0][0])
  409.                     except ValueError:
  410.                         return False
  411.                 else:
  412.                     i = PointConversion(self.segments, args[0]).to_int()
  413.             else:
  414.                 i = int(args[0])
  415.         else:
  416.             i = PointConversion(self.segments, tuple(args)).to_int()
  417.        
  418.         #Add to grid if cell is empty
  419.         if 0 <= i < len(self.grid_data) and self.grid_data[i] not in (0, 1) and i is not None:
  420.             self.grid_data[i] = id
  421.             return i
  422.         else:
  423.             return None
  424.            
  425.            
  426.     def shuffle(self, no_shuffle=None):
  427.         """Mirror the grid in the X, Y, or Z axis."""
  428.        
  429.         shuffle_methods = random.sample(range(3), random.randint(0, 2))
  430.         if 0 in shuffle_methods:
  431.             self.grid_data = SwapGridData(self.grid_data).x()
  432.             if self.range_data is not None:
  433.                 self.range_data = SwapGridData(self.range_data).x()
  434.         if 1 in shuffle_methods:
  435.             self.grid_data = SwapGridData(self.grid_data).y()
  436.             if self.range_data is not None:
  437.                 self.range_data = SwapGridData(self.range_data).y()
  438.         if 2 in shuffle_methods:
  439.             self.grid_data = SwapGridData(self.grid_data).y()
  440.             if self.range_data is not None:
  441.                 self.range_data = SwapGridData(self.range_data).y()
  442.         self.grid_data.reverse()
  443.         if self.range_data is not None:
  444.             self.range_data.reverse()
  445.            
  446.            
  447.            
  448.     def update_score(self):
  449.         """Recalculate the score.
  450.        
  451.        There are 26 total directions from each point, or 13 lines, calculated in
  452.        the DirectionCalculation() class. For each of the 13 lines, look both ways
  453.        and count the number of values that match the current player.
  454.        
  455.        This will find any matches from one point, so it's simple to then iterate
  456.        through every point. A hash of each line is stored to avoid duplicates.
  457.        """
  458.         try:
  459.             self.grid_data_last_updated
  460.         except AttributeError:
  461.             self.grid_data_last_updated = None
  462.        
  463.  
  464.         if self.grid_data_last_updated != self.grid_data or True:
  465.            
  466.             #Store hash of grid_data in it's current state to avoid unnecessarily running the code again when there's been no changes
  467.             self.grid_data_last_updated = self.grid_data
  468.            
  469.            
  470.             self.current_points = defaultdict(int)
  471.             all_matches = set()
  472.            
  473.             #Loop through each point
  474.             for starting_point in range(len(self.grid_data)):
  475.                
  476.                 current_player = self.grid_data[starting_point]
  477.                
  478.                 #Fix for the pygame temporary numbers
  479.                 current_player_fixed = None
  480.                 if type(current_player) == int:
  481.                     current_player_fixed = 9 - current_player
  482.                    
  483.                 if current_player != '':
  484.                
  485.                     for i in DirectionCalculation().opposite_direction:
  486.                        
  487.                         #Get a list of directions and calculate movement amount
  488.                         possible_directions = [list(i)]
  489.                         possible_directions += [[j.replace(i, '') for i in possible_directions[0] for j in DirectionCalculation().direction_group.values() if i in j]]
  490.                         direction_movement = sum(self.direction_maths[j] for j in possible_directions[0])
  491.                        
  492.                         #Build list of invalid directions
  493.                         invalid_directions = [[self.direction_edges[j] for j in possible_directions[k]] for k in (0, 1)]
  494.                         invalid_directions = [join_list(j) for j in invalid_directions]
  495.                        
  496.                         num_matches = 1
  497.                         list_match = [starting_point]
  498.                        
  499.                         #Use two loops for the opposite directions
  500.                         for j in (0, 1):
  501.                            
  502.                             current_point = starting_point
  503.                            
  504.                             while current_point not in invalid_directions[j] and 0 < current_point < len(self.grid_data):
  505.                                 current_point += direction_movement * int('-'[:j] + '1')
  506.                                
  507.                                 if self.grid_data[current_point] == current_player:
  508.                                     num_matches += 1
  509.                                     list_match.append(current_point)
  510.                                 else:
  511.                                     break
  512.                        
  513.                         #Add a point if enough matches
  514.                         if num_matches == self.segments:
  515.                             list_match = hash(tuple(sorted(list_match)))
  516.                             if list_match not in all_matches:
  517.                                 all_matches.add(list_match)
  518.                                 self.current_points[current_player] += 1
  519.  
  520.  
  521.     def show_score(self, digits=False, marker='/'):
  522.         """Print the current points.
  523.        
  524.        Parameters:
  525.            digits (bool, optional): If the score should be output as a number,
  526.                or as individual marks.
  527.            
  528.            marker (str, optional): How each point should be displayed if
  529.                digits are not being used.
  530.        
  531.        >>> C3D = Connect3D()
  532.        >>> C3D.update_score()
  533.        >>> C3D.current_points['X'] = 5
  534.        >>> C3D.current_points['O'] = 1
  535.        
  536.        >>> C3D.show_score(False, '/')
  537.        'Player X: /////  Player O: /'
  538.        >>> C3D.show_score(True)
  539.        'Player X: 5  Player O: 1'
  540.        """
  541.         self.update_score()
  542.         multiply_value = 1 if digits else marker
  543.         return 'Player X: {x}  Player O: {o}'.format(x=multiply_value*(self.current_points['X']), o=multiply_value*self.current_points['O'])
  544.    
  545.        
  546.     def reset(self):
  547.         """Empty the grid without creating a new Connect3D object."""
  548.         self.grid_data = ['' for i in range(pow(self.segments, 3))]
  549.  
  550.  
  551. class DirectionCalculation(object):
  552.     """Calculate which directions are possible to move in, based on the 6 directions.
  553.    Any combination is fine, as long as it doesn't go back on itself, hence why X, Y
  554.    and Z have been given two values each, as opposed to just using six values.
  555.    
  556.    Because the code to calculate score will look in one direction then reverse it,
  557.    the list then needs to be trimmed down to remove any duplicate directions (eg.
  558.    up/down and upright/downleft are both duplicates)
  559.    
  560.    The code will output the following results, it is possible to use these instead of the class.
  561.        direction_group = {'Y': 'UD', 'X': 'LR', 'Z': 'FB', ' ': ' '}
  562.        opposite_direction = ('B', 'D', 'DF', 'LDB', 'DB', 'L', 'LUB', 'LUF', 'LF', 'RU', 'LB', 'LDF', 'RD')
  563.    """
  564.    
  565.     direction_group = {}
  566.     direction_group['X'] = 'LR'
  567.     direction_group['Y'] = 'UD'
  568.     direction_group['Z'] = 'FB'
  569.     direction_group[' '] = ' '
  570.    
  571.     #Come up with all possible directions
  572.     all_directions = set()
  573.     for x in [' ', 'X']:
  574.         for y in [' ', 'Y']:
  575.             for z in [' ', 'Z']:
  576.                 x_directions = list(direction_group[x])
  577.                 y_directions = list(direction_group[y])
  578.                 z_directions = list(direction_group[z])
  579.                 for i in x_directions:
  580.                     for j in y_directions:
  581.                         for k in z_directions:
  582.                             all_directions.add((i+j+k).replace(' ', ''))
  583.    
  584.     #Narrow list down to remove any opposite directions
  585.     opposite_direction = all_directions.copy()
  586.     for i in all_directions:
  587.         if i in opposite_direction:
  588.             new_direction = ''
  589.             for j in list(i):
  590.                 for k in direction_group.values():
  591.                     if j in k:
  592.                         new_direction += k.replace(j, '')
  593.             opposite_direction.remove(new_direction)
  594.  
  595.  
  596. class PointConversion(object):
  597.     """Used to convert the cell ID to 3D coordinates or vice versa.
  598.    Mainly used for inputting the coordinates to make a move.
  599.    
  600.    The cell ID is from 0 to segments^3, and coordinates are from 1 to segments.
  601.    This means an ID of 0 is actually (1,1,1), and 3 would be (4,1,1).
  602.    
  603.               - X -
  604.             __1___2_
  605.        /  1/ 0 / 1 /|
  606.       Y   /___/___/ |
  607.      /  2/ 2 / 3 /  |
  608.         /___/___/   |
  609.        |   |____|___|
  610.     | 1|   / 4 /|5 /
  611.     Z  |  /___/_|_/
  612.     | 2| / 6 / 7|/
  613.        |/___/___|
  614.    
  615.    Parameters:
  616.        segments:
  617.            Size of the grid.
  618.            Type: int
  619.        
  620.        i:
  621.            Cell ID or coordinates.
  622.            Type int/tuple/list
  623.    
  624.    Functions:
  625.        to_3d
  626.        to_int
  627.    """
  628.     def __init__(self, segments, i):
  629.         self.segments = segments
  630.         self.i = i
  631.        
  632.     def to_3d(self):
  633.         """Convert cell ID to a 3D coordinate.
  634.        
  635.        >>> segments = 4
  636.        >>> cell_id = 16
  637.        
  638.        >>> PointConversion(segments, cell_id).to_3d()
  639.        (1, 1, 2)
  640.        """
  641.         cell_id = int(self.i)
  642.         z = cell_id / pow(self.segments, 2)
  643.         cell_id %= pow(self.segments, 2)
  644.         y = cell_id / self.segments
  645.         x = cell_id % self.segments
  646.         return tuple(cell_id+1 for cell_id in (x, y, z))
  647.    
  648.     def to_int(self):
  649.         """Convert 3D coordinates to the cell ID.
  650.        
  651.        >>> segments = 4
  652.        >>> coordinates = (4,2,3)
  653.        
  654.        >>> PointConversion(segments, coordinates).to_int()
  655.        39
  656.        """
  657.         x, y, z = [int(i) for i in self.i]
  658.         if all(i > 0 for i in (x, y, z)):
  659.             return (x-1)*pow(self.segments, 0) + (y-1)*pow(self.segments, 1) + (z-1)*pow(self.segments, 2)
  660.         return None
  661.  
  662.  
  663. class SwapGridData(object):
  664.     """Use the size of the grid to calculate how flip it on the X, Y, or Z axis.
  665.    The flips keep the grid intact but change the perspective of the game.
  666.    
  667.    Parameters:
  668.        grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.
  669.    """
  670.     def __init__(self, grid_data):
  671.         self.grid_data = list(grid_data)
  672.         self.segments = calculate_segments(self.grid_data)
  673.    
  674.     def x(self):
  675.         """Flip on the X axis.
  676.        
  677.        >>> SwapGridData(range(8)).x()
  678.        [1, 0, 3, 2, 5, 4, 7, 6]
  679.        >>> print Connect3D.from_list(SwapGridData(range(8)).x())
  680.             ________
  681.            / 1 / 0 /|
  682.           /___/___/ |
  683.          / 3 / 2 /  |
  684.         /___/___/   |
  685.        |   |____|___|
  686.        |   / 5 /|4 /
  687.        |  /___/_|_/
  688.        | / 7 / 6|/
  689.        |/___/___|
  690.        """
  691.         return join_list(x[::-1] for x in split_list(self.grid_data, self.segments))
  692.        
  693.     def y(self):
  694.         """Flip on the Y axis.
  695.        
  696.        >>> SwapGridData(range(8)).y()
  697.        [2, 3, 0, 1, 6, 7, 4, 5]
  698.        >>> print Connect3D.from_list(SwapGridData(range(8)).y())
  699.             ________
  700.            / 2 / 3 /|
  701.           /___/___/ |
  702.          / 0 / 1 /  |
  703.         /___/___/   |
  704.        |   |____|___|
  705.        |   / 6 /|7 /
  706.        |  /___/_|_/
  707.        | / 4 / 5|/
  708.        |/___/___|
  709.        """
  710.         group_split = split_list(self.grid_data, pow(self.segments, 2))
  711.         return join_list(join_list(split_list(x, self.segments)[::-1]) for x in group_split)
  712.        
  713.     def z(self):
  714.         """Flip on the Z axis.
  715.        
  716.        >>> SwapGridData(range(8)).z()
  717.        [4, 5, 6, 7, 0, 1, 2, 3]
  718.        >>> print Connect3D.from_list(SwapGridData(range(8)).z())
  719.             ________
  720.            / 4 / 5 /|
  721.           /___/___/ |
  722.          / 6 / 7 /  |
  723.         /___/___/   |
  724.        |   |____|___|
  725.        |   / 0 /|1 /
  726.        |  /___/_|_/
  727.        | / 2 / 3|/
  728.        |/___/___|
  729.        """
  730.         return join_list(split_list(self.grid_data, pow(self.segments, 2))[::-1])
  731.    
  732.     def reverse(self):
  733.         """Reverse the grid.
  734.        
  735.        >>> SwapGridData(range(8)).reverse()
  736.        [7, 6, 5, 4, 3, 2, 1, 0]
  737.        >>> print Connect3D.from_list(SwapGridData(range(8)).reverse())
  738.             ________
  739.            / 7 / 6 /|
  740.           /___/___/ |
  741.          / 5 / 4 /  |
  742.         /___/___/   |
  743.        |   |____|___|
  744.        |   / 3 /|2 /
  745.        |  /___/_|_/
  746.        | / 1 / 0|/
  747.        |/___/___|
  748.        """
  749.         return self.grid_data[::-1]
  750.  
  751.  
  752. def calculate_segments(grid_data):
  753.     """Cube root the length of grid_data to find the grid size."""
  754.     return int(round(pow(len(grid_data), 1.0/3.0), 0))
  755.  
  756.  
  757. def split_list(x, n):
  758.     """Split a list by n characters."""
  759.     n = int(n)
  760.     return [x[i:i+n] for i in range(0, len(x), n)]
  761.    
  762.    
  763. def join_list(x):
  764.     """Convert nested lists into one single list."""
  765.     return [j for i in x for j in i]
  766.        
  767.        
  768. def get_max_dict_keys(x):
  769.     """Return a list of every key containing the max value.
  770.    
  771.    Parameters:
  772.        x (dict): Dictionary to sort and get highest value.
  773.            It must be a dictionary of integers to work properly.
  774.    """
  775.     if x:
  776.         sorted_dict = sorted(x.iteritems(), key=operator.itemgetter(1), reverse=True)
  777.         if sorted_dict[0][1]:
  778.             return sorted([k for k, v in x.iteritems() if v == sorted_dict[0][1]])
  779.     return []
  780.  
  781.    
  782. def get_bot_difficulty(level, _default=Connect3D.bot_difficulty_default, _debug=False):
  783.     """Preset parameters for the bot difficulty levels.
  784.    
  785.    Parameters:
  786.        level (str/int): Difficulty level to get the data for.
  787.        
  788.        _default (str/int): If level is invalid, use this as the default value.
  789.    
  790.    There are 3 variables to control the chance of doing something differently:
  791.        Changing Tactic - Normally the computer will give priority to blocking an
  792.            enemy row of n-1 before completing it's own, and adding to a row of
  793.            n-2 before blocking the enemy. This is the percent chance to override
  794.            this behavior.
  795.        
  796.        Not Noticing - The chance the computer will miss a row that is almost complete.
  797.            Without this, the computer will be able to block absolutely everything
  798.            unless it is tricked.
  799.            Leave this quite high for the n-2 rows, since it can be frustrating to
  800.            have every row blocked before you've even half finished it.
  801.        
  802.        Not Noticing Divide - Not noticing rows of n-2 keeps the game flowing,
  803.            not noticing rows of n-1 makes it too easy to win. This will reduce the
  804.            'Not Noticing' chance for rows of n-1 so the computer doesn't look
  805.            like it's totally blind.
  806.        
  807.        In addition to the 'Not Noticing Divide', as the grid is more empty and is
  808.        easy to see, the chance of not noticing something is reduced.    
  809.    """
  810.     difficulty_level = {}
  811.     difficulty_level[0] = 'beginner'
  812.     difficulty_level[1] = 'easy'
  813.     difficulty_level[2] = 'medium'
  814.     difficulty_level[3] = 'hard'
  815.     difficulty_level[4] = 'extreme'
  816.    
  817.     try:
  818.         level = difficulty_level[level]
  819.     except KeyError:
  820.         level = str(level).lower()
  821.    
  822.     if level == difficulty_level[0]:
  823.         if _debug:
  824.             return 1
  825.         return (75, 95, 1)
  826.     elif level == difficulty_level[1]:
  827.         if _debug:
  828.             return 2
  829.         return (50, 75, 2)
  830.     elif level == difficulty_level[2]:
  831.         if _debug:
  832.             return 3
  833.         return (40, 40, 4)
  834.     elif level == difficulty_level[3]:
  835.         if _debug:
  836.             return 4
  837.         return (20, 20, 4)
  838.     elif level == difficulty_level[4]:
  839.         if _debug:
  840.             return 5
  841.         return (0, 0, 1)
  842.    
  843.     return get_bot_difficulty(_default, _debug)
  844.  
  845.    
  846.  
  847. class SimpleC3DAI(object):
  848.     """AI coded to play Connect3D."""
  849.    
  850.     def __init__(self, C3DObject, player_num, difficulty=Connect3D.bot_difficulty_default):
  851.         """Set up the AI for a single move using the current state of Connect3D.
  852.        
  853.        Parameters:
  854.            C3DObject (object): Connect3D object, needed to get the current
  855.                state of the game as opposed to passing in lots of values.
  856.            
  857.            player_num (int): Which player the AI is.
  858.            
  859.            difficulty (string/int): Difficulty level to use for the AI.
  860.        """
  861.         self.C3DObject = C3DObject
  862.         self.player_num = player_num
  863.         self.player = player_num #Connect3D.player_symbols[1]
  864.         self.enemy = int(not self.player_num)# Connect3D.player_symbols[int(not self.player_num)]
  865.         self.gd_len = self.C3DObject.segments_cubed
  866.         self.difficulty = difficulty
  867.         self.grid_data = [i if i in (self.player, self.enemy) else '' for i in C3DObject.grid_data]
  868.        
  869.         self.checks = 0
  870.    
  871.     def max_cell_points(self):
  872.         """Get maximum number of points that can be gained from each empty cell,
  873.        that is not blocked by an enemy value.
  874.        """
  875.         max_points = defaultdict(int)
  876.         filled_grid_data = [i if i != '' else self.player for i in self.grid_data]
  877.         for cell_id in range(self.gd_len):
  878.             self.checks += 1            
  879.             if filled_grid_data[cell_id] == self.player and self.grid_data[cell_id] == '':
  880.                 max_points[cell_id] = self.check_grid(filled_grid_data, cell_id, self.player)
  881.                
  882.         return get_max_dict_keys(max_points)
  883.    
  884.     def check_for_n_minus_one(self, grid_data=None):
  885.         """Find all places where anyone has n-1 points in a row, by substituting
  886.        in a point for each player in every cell.
  887.        
  888.        Parameters:
  889.            grid_data (list or None, optional): Pass in a custom grid_data,
  890.                leave as None to use the Connect3D one.
  891.        """
  892.        
  893.         if grid_data is None:
  894.             grid_data = list(self.grid_data)
  895.        
  896.         matches = defaultdict(list)
  897.         for cell_id in range(len(grid_data)):
  898.             if grid_data[cell_id] == '':
  899.                 for current_player in (self.player, self.enemy):
  900.                     if self.check_grid(grid_data, cell_id, current_player):
  901.                         matches[current_player].append(cell_id)
  902.         return matches
  903.    
  904.     def look_ahead(self):
  905.         """Look two moves ahead to detect if someone could get a point.
  906.        Uses the check_for_n_minus_one function from within a loop.
  907.        
  908.        Will return 1 as the second parameter if it has looked up more than a single move.
  909.        """
  910.         #Try initial check
  911.         match = self.check_for_n_minus_one()
  912.         if match:
  913.             return (match, 0)
  914.            
  915.         #For every grid cell, substitute a player into it, then do the check again
  916.         grid_data = list(self.grid_data)
  917.         matches = defaultdict(list)
  918.         for i in range(self.gd_len):
  919.             if self.C3DObject.grid_data[i] == '':
  920.                 old_value = grid_data[i]
  921.                 for current_player in (self.player, self.enemy):
  922.                     grid_data[i] = current_player
  923.                     match = self.check_for_n_minus_one(grid_data)
  924.                     if match:
  925.                         for k, v in match.iteritems():
  926.                             matches[k] += v
  927.                            
  928.                 grid_data[i] = old_value
  929.                
  930.         if matches:
  931.             return (matches, 1)
  932.            
  933.         return (defaultdict(list), 0)
  934.    
  935.     def check_grid(self, grid_data, cell_id, player):
  936.         """Duplicate of the Connect3D.update_score method, but set up to check individual cells.
  937.        
  938.        Parameters:
  939.            grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.
  940.            
  941.            cell_id (int): The cell ID, or grid_data index to update.
  942.            
  943.            player (int): Integer representation of the player, can be 0 or 1.
  944.        """
  945.         max_points = 0
  946.         for i in DirectionCalculation().opposite_direction:
  947.            
  948.             #Get a list of directions and calculate movement amount
  949.             possible_directions = [list(i)]
  950.             possible_directions += [[j.replace(i, '') for i in possible_directions[0] for j in DirectionCalculation().direction_group.values() if i in j]]
  951.             direction_movement = sum(self.C3DObject.direction_maths[j] for j in possible_directions[0])
  952.            
  953.             #Build list of invalid directions
  954.             invalid_directions = [[self.C3DObject.direction_edges[j] for j in possible_directions[k]] for k in (0, 1)]
  955.             invalid_directions = [join_list(j) for j in invalid_directions]
  956.            
  957.             num_matches = 1
  958.            
  959.             #Use two loops for the opposite directions
  960.             for j in (0, 1):
  961.                
  962.                 current_point = cell_id
  963.                
  964.                 while current_point not in invalid_directions[j] and 0 <= current_point < len(grid_data):
  965.                
  966.                     self.checks += 1
  967.                     current_point += direction_movement * int('-'[:j] + '1')
  968.                     if grid_data[current_point] == player:
  969.                         num_matches += 1
  970.                     else:
  971.                         break
  972.  
  973.             #Add a point if enough matches
  974.             if num_matches == self.C3DObject.segments:
  975.                 max_points += 1
  976.                      
  977.         return max_points
  978.    
  979.     def calculate_next_move(self):
  980.         """Groups together the AI methods in order of importance.
  981.        Will throw an error if grid_data is full, since the game should have ended by then anyway.
  982.        
  983.        The far_away part determins which order to do things in.
  984.        
  985.            It's set up so that for n-1 in a row, the most urgent thing is to stop the
  986.            opposing player before gaining any points. However, for n-2 in a row, it's
  987.            more useful to gain points if possible.
  988.            
  989.            By setting order_of_importance to 0, it'll always try block the player
  990.            first, and by setting to 1, it'll always try score points regardless of
  991.            if the other player will get one too.
  992.        """
  993.        
  994.         chance_of_changing_tactic, chance_of_not_noticing, chance_of_not_noticing_divide = get_bot_difficulty(self.difficulty)
  995.        
  996.         grid_data_joined_len = len(''.join(map(str, self.C3DObject.grid_data)))
  997.        
  998.         next_moves = []
  999.         self.C3DObject.ai_message = []
  1000.         ai_message = self.C3DObject.ai_message.append
  1001.         self.checks = 0
  1002.         state = None
  1003.        
  1004.         if grid_data_joined_len > (self.C3DObject.segments - 2) * 2 - 1:
  1005.            
  1006.             point_based_move, far_away = self.look_ahead()
  1007.            
  1008.             #Format debug message
  1009.             possible_moves_type = ['Block', 'Gain']
  1010.             if not self.player:
  1011.                 possible_moves_type = possible_moves_type[::-1]
  1012.             for k, v in point_based_move.iteritems():
  1013.                 if k == self.player:
  1014.                     message = 'Gain Moves: {}'
  1015.                 elif k == self.enemy:
  1016.                     message = 'Block Moves: {}'
  1017.                 self.C3DObject.ai_message.append(message.format(v))
  1018.                    
  1019.            
  1020.             #self.C3DObject.ai_message.append('Possible Moves: {}'.format(possible_moves_message))
  1021.             self.C3DObject.ai_message.append('Urgent: {}'.format(not far_away))
  1022.            
  1023.             #Reduce chance of not noticing n-1 in a row, since n-2 in a row isn't too important
  1024.             if not far_away:
  1025.                 chance_of_not_noticing /= chance_of_not_noticing_divide
  1026.                 chance_of_not_noticing = pow(chance_of_not_noticing, pow(grid_data_joined_len / float(len(self.C3DObject.grid_data)), 0.4))
  1027.            
  1028.            
  1029.             #Set which order to do things in
  1030.             order_of_importance = int('-'[:int(far_away)] + '1')
  1031.            
  1032.             ai_new_tactic = random.uniform(0, 100) < chance_of_changing_tactic
  1033.             if ai_new_tactic:
  1034.                 ai_message('AI changed tacic.')
  1035.                 order_of_importance = random.choice((-1, 1))
  1036.                
  1037.             move1_player = [self.enemy, self.player][::order_of_importance]
  1038.             move1_text = ['Blocking opposing player', 'Gaining points'][::order_of_importance]
  1039.            
  1040.            
  1041.             #Predict if other player is trying to trick the AI
  1042.             #(or try trick the player if the setup is right)
  1043.             #Quite an advanced tactic so chance of not noticing is increased
  1044.             if min(random.uniform(0, 100), random.uniform(0, 100)) > chance_of_not_noticing:
  1045.            
  1046.                 matching_ids = [defaultdict(int) for i in range(3)]
  1047.                 for k, v in point_based_move.iteritems():
  1048.                     for i in v:
  1049.                         matching_ids[k][i] += 1
  1050.                         matching_ids[2][i] += 1
  1051.                
  1052.                 if matching_ids[2]:
  1053.                    
  1054.                     if matching_ids[move1_player[0]]:
  1055.                         highest_occurance = max(matching_ids[move1_player[0]].iteritems(), key=operator.itemgetter(1))[1]
  1056.                         if highest_occurance > 1:
  1057.                             next_moves = [k for k, v in matching_ids[move1_player[0]].iteritems() if v == highest_occurance]
  1058.                             state = 'Forward thinking ({})'.format(move1_text[0])
  1059.                        
  1060.                     if not next_moves and matching_ids[move1_player[1]]:
  1061.                         highest_occurance = max(matching_ids[move1_player[1]].iteritems(), key=operator.itemgetter(1))[1]
  1062.                         if highest_occurance > 1:
  1063.                             next_moves = [k for k, v in matching_ids[move1_player[1]].iteritems() if v == highest_occurance]
  1064.                             state = 'Forward thinking ({})'.format(move1_text[1])
  1065.                    
  1066.                     if not next_moves:
  1067.                         highest_occurance = max(matching_ids[2].iteritems(), key=operator.itemgetter(1))[1]
  1068.                         if highest_occurance > 1:
  1069.                             next_moves = [k for k, v in matching_ids[2].iteritems() if v == highest_occurance]
  1070.                             state = 'Forward thinking'
  1071.            
  1072.            
  1073.             #Make a move based on other points
  1074.             ai_noticed = random.uniform(0, 100) > chance_of_not_noticing
  1075.             if point_based_move and ai_noticed and (not next_moves or not far_away):
  1076.                 if point_based_move[move1_player[0]]:
  1077.                     next_moves = point_based_move[move1_player[0]]
  1078.                     state = move1_text[0]
  1079.                    
  1080.                 elif point_based_move[move1_player[1]]:
  1081.                     next_moves = point_based_move[move1_player[1]]
  1082.                     state = move1_text[1]
  1083.            
  1084.             #Make a random move determined by number of possible points
  1085.             elif not state:
  1086.                 if not ai_noticed:
  1087.                     ai_message("AI didn't notice something.")
  1088.                 state = False
  1089.                
  1090.         #Make a semi random placement
  1091.         if not state:
  1092.             if not chance_of_not_noticing and random.uniform(0, 100) > chance_of_not_noticing:
  1093.                 next_moves = self.max_cell_points()
  1094.                 state = 'Predictive placement'
  1095.             else:
  1096.                 state = 'Random placement'
  1097.            
  1098.             if state is None:
  1099.                 state = 'Starting'
  1100.            
  1101.         #Make a totally random move
  1102.         if not next_moves:
  1103.             next_moves = [i for i in range(self.gd_len) if self.grid_data[i] == '']
  1104.             if state is None:
  1105.                 state = 'Struggling'
  1106.            
  1107.        
  1108.         ai_message('AI Objective: {}.'.format(state))
  1109.         n = random.choice(next_moves)
  1110.        
  1111.         if len(next_moves) != len(self.grid_data) - len(''.join(map(str, self.grid_data))):
  1112.             ai_message('Potential Moves: {}'.format(next_moves))
  1113.        
  1114.         next_move = random.choice(next_moves)
  1115.        
  1116.         ai_message('Chosen Move: {}'.format(next_move))
  1117.         ai_message('Calculations: {}'.format(self.checks + 1))
  1118.        
  1119.         return next_move
  1120.  
  1121. class MouseToBlockID(object):
  1122.     """Converts mouse coordinates into the games block ID.
  1123.  
  1124.    The first part is to calculate which level has been clicked, which
  1125.    then allows the code to treat the coordinates as level 0. From this
  1126.    point, it finds the matching chunks from the new coordinates which
  1127.    results in two possible blocks, then it calculates how they are
  1128.    conected (highest one is to the left if even+odd, otherwise it's to
  1129.    the right), and from that, it's possible to figure out which block
  1130.    the cursor is over.
  1131.    
  1132.    A chunk is a cell of a 2D grid overlaid over the isometric grid.
  1133.    Each block is split into 4 chunks, and each chunk overlaps two
  1134.    blocks.
  1135.    """
  1136.    
  1137.     def __init__(self, x, y, grid_main):
  1138.         self.x = x
  1139.         self.y = y
  1140.         self.y_original = y
  1141.         self.grid_main = grid_main
  1142.         self._to_chunk()
  1143.  
  1144.     def _to_chunk(self):
  1145.         """Calculate which chunk the coordinate is on."""
  1146.         y_offset = self.grid_main.size_y * 2 + self.grid_main.padding
  1147.         self.y_coordinate = int((self.grid_main.centre - self.y) / y_offset)
  1148.         self.y += y_offset * self.y_coordinate
  1149.        
  1150.         chunk_size_x = self.grid_main.size_x / self.grid_main.segments
  1151.         chunk_size_y = self.grid_main.size_y / self.grid_main.segments
  1152.         self.height = int((self.grid_main.centre - self.y) / chunk_size_y)
  1153.         self.width = int((self.x + self.grid_main.size_x + chunk_size_x) / chunk_size_x) -1
  1154.        
  1155.  
  1156.     def find_x_slice(self):
  1157.         """Find block IDs that are on the x segment"""
  1158.         past_middle = self.width >= self.grid_main.segments
  1159.        
  1160.         values = []
  1161.         if self.width >= self.grid_main.segments:
  1162.        
  1163.             count = 0
  1164.             while True:
  1165.                 n_multiple = self.grid_main.segments * count
  1166.                 width_addition = self.width - self.grid_main.segments + count
  1167.                 if width_addition < self.grid_main.segments:
  1168.                     values.append(n_multiple + width_addition)
  1169.                     if width_addition < self.grid_main.segments - 1:
  1170.                         values.append(n_multiple + width_addition + 1)
  1171.                        
  1172.                 else:
  1173.                     break
  1174.                 count += 1
  1175.        
  1176.         elif self.width >= 0:
  1177.        
  1178.             starting_point = self.grid_main.segments - self.width
  1179.             values.append((starting_point - 1) * self.grid_main.segments)
  1180.  
  1181.             width_addition = 0
  1182.             for i in range(starting_point, self.grid_main.segments):
  1183.                 n_multiple = self.grid_main.segments * i
  1184.                 values.append(n_multiple + width_addition)
  1185.                 if 0 < i < self.grid_main.segments:
  1186.                     values.append(n_multiple + width_addition + 1)
  1187.                 else:
  1188.                     break
  1189.                 width_addition += 1
  1190.        
  1191.            
  1192.         return values
  1193.  
  1194.     def find_y_slice(self):
  1195.         """Find block IDs that are on the y segment"""
  1196.        
  1197.         height = self.height
  1198.         past_middle = height >= self.grid_main.segments
  1199.         if past_middle:
  1200.             height = 2 * self.grid_main.segments - 1 - height
  1201.            
  1202.         values = []
  1203.         count = 0
  1204.         while True:
  1205.             n_multiple = count * self.grid_main.segments
  1206.             height_addition = height - count
  1207.             if height_addition >= 0:
  1208.                 values.append(n_multiple + height_addition)
  1209.                 if height_addition >= 1:
  1210.                     values.append(n_multiple + height_addition - 1)
  1211.             else:
  1212.                 break
  1213.             count += 1
  1214.            
  1215.         if past_middle:
  1216.             values = [pow(self.grid_main.segments, 2) - i - 1 for i in values]
  1217.            
  1218.         return values
  1219.  
  1220.     def find_overlap(self):
  1221.         """Combine the block IDs to find the 1 or 2 matching ones."""
  1222.        
  1223.         x_blocks = self.find_x_slice()
  1224.         y_blocks = self.find_y_slice()
  1225.         if self.y_coordinate >= self.grid_main.segments:
  1226.             return []
  1227.         return [i for i in x_blocks if i in y_blocks]
  1228.  
  1229.     def find_block_coordinates(self):
  1230.         """Calculate the coordinates of the block IDs, or create a fake
  1231.        block if one is off the edge.
  1232.        Returns a list sorted by height.
  1233.  
  1234.        If only one value is given for which blocks are in the chunk, that
  1235.        means the player is on the edge of the board. By creating a fake
  1236.        block off the side of the board, it allows the coorect maths to be
  1237.        done without any modification.
  1238.        """
  1239.         matching_blocks = self.find_overlap()
  1240.         if not matching_blocks:
  1241.             return None
  1242.        
  1243.         matching_coordinates = {i: self.grid_main.relative_coordinates[i]
  1244.                                 for i in matching_blocks}
  1245.  
  1246.         #Create new value to handle 'off edge' cases
  1247.         if len(matching_coordinates.keys()) == 1:
  1248.            
  1249.             single_coordinate = matching_coordinates[matching_blocks[0]]
  1250.            
  1251.             new_location = (0, -self.grid_main.centre)
  1252.  
  1253.             #Workaround to handle the cases in the upper half
  1254.             if self.height < self.grid_main.segments:
  1255.                
  1256.                 top_row_right = range(1, self.grid_main.segments)
  1257.                 top_row_left = [i * self.grid_main.segments
  1258.                                 for i in range(1, self.grid_main.segments)]
  1259.                 if self.width >= self.grid_main.segments:
  1260.                     top_row_right.append(0)
  1261.                 else:
  1262.                     top_row_left.append(0)
  1263.  
  1264.                
  1265.                 if matching_blocks[0] in top_row_left:
  1266.                     new_location = (single_coordinate[0] - self.grid_main.x_offset,
  1267.                                     single_coordinate[1] + self.grid_main.y_offset)
  1268.  
  1269.                 elif matching_blocks[0] in top_row_right:
  1270.                     new_location = (single_coordinate[0] + self.grid_main.x_offset,
  1271.                                     single_coordinate[1] + self.grid_main.y_offset)
  1272.            
  1273.             matching_coordinates[-1] = new_location
  1274.            
  1275.         return sorted(matching_coordinates.items(), key=lambda (k, v): v[1])
  1276.  
  1277.    
  1278.     def calculate(self, debug=0):
  1279.         """Calculate which block ID the coordinates are on.
  1280.        This calculates the coordinates of the line between the two
  1281.        blocks, then depending on if a calculation results in a positive
  1282.        or negative number, it's possible to detect which block it falls
  1283.        on.
  1284.  
  1285.        By returning the (x1, y1) and (x2, y2) values, they can be linked
  1286.        with turtle to see it how it works under the hood.
  1287.        """
  1288.         all_blocks = self.find_block_coordinates()
  1289.         if all_blocks is None:
  1290.             return None
  1291.        
  1292.         highest_block = all_blocks[1][1]
  1293.         line_direction = self.width % 2 == self.height % 2
  1294.         if self.grid_main.segments % 2:
  1295.             line_direction = not line_direction
  1296.         #print self.width, self.height
  1297.        
  1298.         x1, y1 = (highest_block[0],
  1299.                   highest_block[1] - self.grid_main.y_offset * 2)
  1300.         negative = int('-1'[not line_direction:])
  1301.         x2, y2 = (x1 + self.grid_main.x_offset * negative,
  1302.                   y1 + self.grid_main.y_offset)
  1303.  
  1304.         sign = (x2 - x1) * (self.y - y1) - (y2 - y1) * (self.x - x1)
  1305.         sign *= negative
  1306.  
  1307.         #Return particular things when debugging
  1308.         if debug == 1:
  1309.             return (x1, y1), (x2, y2)
  1310.         if debug == 2:
  1311.             return sign
  1312.  
  1313.         selected_block = all_blocks[sign > 0][0]
  1314.  
  1315.         #If extra block was added, it was -1, so it is invalid
  1316.         if selected_block < 0:
  1317.             return None
  1318.  
  1319.        
  1320.         return selected_block + self.y_coordinate * pow(self.grid_main.segments, 2)
  1321.  
  1322.  
  1323. class CoordinateConvert(object):
  1324.     def __init__(self, width, height):
  1325.         self.width = width
  1326.         self.height = height
  1327.         self.centre = (self.width / 2, self.height / 2)
  1328.  
  1329.     def to_pygame(self, x, y):
  1330.         x = x - self.centre[0]
  1331.         y = self.centre[1] - y
  1332.         return (x, y)
  1333.  
  1334.     def to_canvas(self, x, y):
  1335.         x = x + self.centre[0]
  1336.         y = self.centre[1] - y
  1337.         return (x, y)
  1338.  
  1339. class GridDrawData(object):
  1340.     """Hold the relevant data for the grid, to allow it to be shown."""
  1341.    
  1342.     def __init__(self, length, segments, angle, padding=5):
  1343.         self.length = length
  1344.         self.segments = segments
  1345.         self.angle = angle
  1346.         self.padding = padding
  1347.         self._calculate()
  1348.  
  1349.     def _calculate(self):
  1350.         """Perform the main calculations on the values in __init__.
  1351.        This allows updating any of the values, such as the isometric
  1352.        angle, without creating a new class."""
  1353.        
  1354.         self.size_x = self.length * math.cos(math.radians(self.angle))
  1355.         self.size_y = self.length * math.sin(math.radians(self.angle))
  1356.         self.x_offset = self.size_x / self.segments
  1357.         self.y_offset = self.size_y / self.segments
  1358.         self.chunk_height = self.size_y * 2 + self.padding
  1359.        
  1360.         self.centre = (self.chunk_height / 2) * self.segments - self.padding / 2
  1361.         self.size_x_sm = self.size_x / self.segments
  1362.         self.size_y_sm = self.size_y / self.segments
  1363.  
  1364.         #self.segments_sq = pow(self.segments, 2)
  1365.         #self.grid_data_len = pow(self.segments, 3)
  1366.         #self.grid_data_range = range(self.grid_data_len)
  1367.  
  1368.        
  1369.         self.length_small = self.length / self.segments
  1370.        
  1371.         self.relative_coordinates = []
  1372.         position = (0, self.centre)
  1373.         for j in range(self.segments):
  1374.             checkpoint = position
  1375.             for i in range(self.segments):
  1376.                 self.relative_coordinates.append(position)
  1377.                 position = (position[0] + self.x_offset,
  1378.                             position[1] - self.y_offset)
  1379.             position = (checkpoint[0] - self.x_offset,
  1380.                         checkpoint[1] - self.y_offset)
  1381.  
  1382.  
  1383.  
  1384.         #Absolute coordinates for pygame
  1385.         chunk_coordinates = [(0, - i * self.chunk_height) for i in range(self.segments)]
  1386.  
  1387.         self.line_coordinates = [((self.size_x, self.centre - self.size_y),
  1388.                                   (self.size_x, self.size_y - self.centre)),
  1389.                                  ((-self.size_x, self.centre - self.size_y),
  1390.                                   (-self.size_x, self.size_y - self.centre)),
  1391.                                  ((0, self.centre - self.size_y * 2),
  1392.                                   (0, -self.centre))]
  1393.  
  1394.         for i in range(self.segments):
  1395.  
  1396.             chunk_height = -i * self.chunk_height
  1397.  
  1398.             self.line_coordinates += [((self.size_x, self.centre + chunk_height - self.size_y),
  1399.                                        (0, self.centre + chunk_height - self.size_y * 2)),
  1400.                                       ((-self.size_x, self.centre + chunk_height - self.size_y),
  1401.                                        (0, self.centre + chunk_height - self.size_y * 2))]
  1402.  
  1403.             for coordinate in self.relative_coordinates:
  1404.                
  1405.                 start = (coordinate[0], chunk_height + coordinate[1])
  1406.                 self.line_coordinates += [(start,
  1407.                                            (start[0] + self.size_x_sm, start[1] - self.size_y_sm)),
  1408.                                           (start,
  1409.                                            (start[0] - self.size_x_sm, start[1] - self.size_y_sm))]
  1410.  
  1411. class RunPygame(object):
  1412.    
  1413.     overlay_marker = '/'
  1414.     player_colours = [GREEN, LIGHTBLUE]
  1415.     empty_colour = YELLOW
  1416.     fps_idle = 15
  1417.     fps_main = 30
  1418.     fps_smooth = 120
  1419.     padding = (5, 10)
  1420.     overlay_width = 500
  1421.     option_padding = 2
  1422.    
  1423.     def __init__(self, C3DObject, screen_width=640, screen_height=860, default_length=200, default_angle=24):
  1424.         self.C3DObject = C3DObject
  1425.         self.width = screen_width
  1426.         self.height = screen_height
  1427.         self.length = default_length
  1428.         self.angle = default_angle
  1429.         self.player = int(not self.C3DObject.current_player)
  1430.        
  1431.         self.convert = CoordinateConvert(self.width, self.height)
  1432.         self.to_pygame = self.convert.to_pygame
  1433.         self.to_canvas = self.convert.to_canvas
  1434.        
  1435.     def _next_player(self):
  1436.         self.player = int(not self.player)
  1437.    
  1438.     def _previous_player(self):
  1439.         self._next_player()
  1440.    
  1441.     def play(self, p1=False, p2=Connect3D.bot_difficulty_default, allow_shuffle=True, end_when_no_points_left=False):
  1442.    
  1443.    
  1444.         #Setup pygame
  1445.         pygame.init()
  1446.         self.screen = pygame.display.set_mode((self.width, self.height))
  1447.         self.clock = pygame.time.Clock()
  1448.         pygame.display.set_caption('Connect 3D')
  1449.         background_colour = BACKGROUND
  1450.         self.backdrop = pygame.Surface((self.width, self.height))
  1451.         self.backdrop.set_alpha(196)
  1452.         self.backdrop.fill(WHITE)
  1453.        
  1454.         #Import the font
  1455.         self.font_file = 'Miss Monkey.ttf'
  1456.         try:
  1457.             pygame.font.Font(self.font_file, 0)
  1458.         except IOError:
  1459.             raise IOError('unable to load font - download from http://www.dafont.com/miss-monkey.font')
  1460.         self.font_lg = pygame.font.Font(self.font_file, 36)
  1461.         self.font_lg_size = self.font_lg.render('', 1, BLACK).get_rect()[3]
  1462.         self.font_md = pygame.font.Font(self.font_file, 24)
  1463.         self.font_md_size = self.font_md.render('', 1, BLACK).get_rect()[3]
  1464.         self.font_sm = pygame.font.Font(self.font_file, 18)
  1465.         self.font_sm_size = self.font_sm.render('', 1, BLACK).get_rect()[3]
  1466.            
  1467.         self.draw_data = GridDrawData(self.length,
  1468.                                  self.C3DObject.segments,
  1469.                                  self.angle,
  1470.                                  padding = self.angle / self.C3DObject.segments)
  1471.        
  1472.         #NOTE: These will all be cleaned up later, the grouping isn't great currently
  1473.        
  1474.         held_keys = {'angle': 0,
  1475.                      'size': 0}
  1476.                      
  1477.         #Store one off instructions to wipe later
  1478.         game_flags = {'clicked': False,
  1479.                       'mouse_used': True,
  1480.                       'quit': False,
  1481.                       'recalculate': False,
  1482.                       'reset': False,
  1483.                       'hover': False,
  1484.                       'flipped': False,
  1485.                       'disable_background_clicks': False,
  1486.                       'winner': None}
  1487.                      
  1488.         #Store information that shouldn't be wiped
  1489.         game_data = {'players': [p1, p2],
  1490.                      'overlay': 'options',
  1491.                      'move_number': 0,
  1492.                      'shuffle': [allow_shuffle, 3],
  1493.                      'debug': False}
  1494.        
  1495.         #Store temporary things to update
  1496.         store_data = {'waiting': False,
  1497.                       'waiting_start': 0,
  1498.                       'shuffle_count': 0,
  1499.                       'temp_fps': self.fps_main,
  1500.                       'player_hover': None,
  1501.                       'shuffle_hover': None,
  1502.                       'new_game': False,
  1503.                       'continue': False,
  1504.                       'exit': False,
  1505.                       'instructions': False,
  1506.                       'debug_hover': None}
  1507.         block_data = {'id': None,
  1508.                       'object': None,
  1509.                       'taken': False}
  1510.         tick_data = {'old': 0,
  1511.                      'new': 0,
  1512.                      'update': 4, #How many ticks between each held key command
  1513.                      'total': 0}
  1514.                      
  1515.         mouse_data = pygame.mouse.get_pos()
  1516.                      
  1517.         #How long to wait before accepting a move
  1518.         moving_wait = 0.5
  1519.        
  1520.         #For controlling how the angle and length of grid update
  1521.         angle_increment = 0.25
  1522.         angle_max = 35
  1523.         length_exponential = 1.1
  1524.         length_increment = 0.5
  1525.         length_multiplier = 0.01
  1526.         time_current = time.time()
  1527.         time_update = 0.01
  1528.        
  1529.         while True:
  1530.                    
  1531.             self.clock.tick(store_data['temp_fps'] or self.fps_idle)
  1532.             tick_data['new'] = pygame.time.get_ticks()
  1533.            
  1534.             if game_flags['quit']:
  1535.                 return self.C3DObject
  1536.            
  1537.             #Check if no spaces are left
  1538.             if '' not in self.C3DObject.grid_data:
  1539.                 game_flags['winner'] = self.C3DObject._get_winning_player()
  1540.                 print 'finish this'
  1541.        
  1542.             #Reset loop
  1543.             self.screen.fill(background_colour)
  1544.             if tick_data['total']:
  1545.                 game_flags['recalculate'] = False
  1546.                 game_flags['mouse_used'] = False
  1547.                 game_flags['clicked'] = False
  1548.                 game_flags['flipped'] = False
  1549.                 game_flags['disable_background_clicks'] = False
  1550.                 store_data['temp_fps'] = None
  1551.             tick_data['total'] += 1
  1552.            
  1553.            
  1554.             #Reinitialise the grid
  1555.             if game_flags['reset']:
  1556.                 game_flags['reset'] = False
  1557.                 game_data['move_number'] = 0
  1558.                 game_data['shuffle'][0] = allow_shuffle
  1559.                 game_data['players'] = (p1, p2)
  1560.                 self.C3DObject = Connect3D(self.C3DObject.segments)
  1561.                 game_flags['hover'] = None
  1562.                 game_flags['recalculate'] = True
  1563.                 store_data['waiting'] = False
  1564.                 game_flags['winner'] = None
  1565.                
  1566.                
  1567.             if game_flags['hover'] is not None:
  1568.                 if self.C3DObject.grid_data[game_flags['hover']] == self.overlay_marker:
  1569.                     self.C3DObject.grid_data[game_flags['hover']] = ''
  1570.                 game_flags['hover'] = None
  1571.            
  1572.             if game_data['overlay']:
  1573.                 game_flags['disable_background_clicks'] = True
  1574.            
  1575.             #Delay each go
  1576.             if store_data['waiting']:                    
  1577.                 game_flags['disable_background_clicks'] = True
  1578.                
  1579.                 if store_data['waiting_start'] < time.time():
  1580.                     game_flags['recalculate'] = True
  1581.                
  1582.                     attempted_move = self.C3DObject.make_move(store_data['waiting'][1], store_data['waiting'][0])
  1583.                    
  1584.                     if attempted_move is not None:
  1585.                         game_data['move_number'] += 1
  1586.                         self.C3DObject.update_score()
  1587.                         store_data['shuffle_count'] += 1
  1588.                        
  1589.                         if store_data['shuffle_count'] >= game_data['shuffle'][1] and game_data['shuffle'][0]:
  1590.                             store_data['shuffle_count'] = 0
  1591.                             self.C3DObject.shuffle()
  1592.                             game_flags['flipped'] = True
  1593.                         else:
  1594.                             game_flags['flipped'] = False
  1595.                            
  1596.                     else:
  1597.                         self._next_player()
  1598.                         print "Invalid move: {}".format(store_data['waiting'][0])
  1599.                        
  1600.                     store_data['waiting'] = False
  1601.                    
  1602.                 else:
  1603.                     try:
  1604.                         self.C3DObject.grid_data[store_data['waiting'][0]] = 9 - store_data['waiting'][1]
  1605.                     except TypeError:
  1606.                         print store_data['waiting'], ai_turn
  1607.                         raise TypeError('trying to get to the bottom of this')
  1608.                    
  1609.                
  1610.             #Run the AI
  1611.             ai_turn = None
  1612.             if game_data['players'][self.player] is not False:
  1613.                 if not game_flags['disable_background_clicks']:
  1614.                     ai_turn = SimpleC3DAI(self.C3DObject, self.player, difficulty=game_data['players'][self.player]).calculate_next_move()
  1615.                
  1616.            
  1617.             #Event loop
  1618.             for event in pygame.event.get():
  1619.  
  1620.                 if event.type == pygame.QUIT:
  1621.                     return
  1622.  
  1623.                 #Get single key presses
  1624.                 if event.type == pygame.KEYDOWN:
  1625.                     game_flags['recalculate'] = True
  1626.  
  1627.                     if event.key == pygame.K_ESCAPE:
  1628.                         if game_data['overlay'] is None:
  1629.                             game_data['overlay'] = 'options'
  1630.                         else:
  1631.                             game_data['overlay'] = None
  1632.                    
  1633.                     if event.key == pygame.K_RIGHTBRACKET:
  1634.                         self.C3DObject.segments += 1
  1635.                         game_flags['reset'] = True
  1636.                        
  1637.                     if event.key == pygame.K_LEFTBRACKET:
  1638.                         self.C3DObject.segments -= 1
  1639.                         self.C3DObject.segments = max(1, self.C3DObject.segments)
  1640.                         game_flags['reset'] = True
  1641.  
  1642.                     if event.key == pygame.K_UP:
  1643.                         held_keys['angle'] = 1
  1644.  
  1645.                     if event.key == pygame.K_DOWN:
  1646.                         held_keys['angle'] = -1
  1647.  
  1648.                     if event.key == pygame.K_RIGHT:
  1649.                         held_keys['size'] = 1
  1650.  
  1651.                     if event.key == pygame.K_LEFT:
  1652.                         held_keys['size'] = -1
  1653.  
  1654.                        
  1655.                 #Get mouse clicks
  1656.                 if event.type == pygame.MOUSEBUTTONDOWN:
  1657.                     game_flags['clicked'] = event.button
  1658.                     game_flags['mouse_used'] = True
  1659.                
  1660.                 if event.type == pygame.MOUSEMOTION:
  1661.                     game_flags['mouse_used'] = True
  1662.            
  1663.            
  1664.             #Get held down key presses, but only update if enough ticks have passed
  1665.             key = pygame.key.get_pressed()
  1666.             update_yet = False
  1667.             if tick_data['new'] - tick_data['old'] > tick_data['update']:
  1668.                 update_yet = True
  1669.                 tick_data['old'] = pygame.time.get_ticks()
  1670.                
  1671.             if held_keys['angle']:
  1672.                
  1673.                 if not (key[pygame.K_UP] or key[pygame.K_DOWN]):
  1674.                     held_keys['angle'] = 0
  1675.                    
  1676.                 elif update_yet:
  1677.                     self.draw_data.angle += angle_increment * held_keys['angle']
  1678.                     game_flags['recalculate'] = True
  1679.                     store_data['temp_fps'] = self.fps_smooth
  1680.            
  1681.             if held_keys['size']:
  1682.                 if not (key[pygame.K_LEFT] or key[pygame.K_RIGHT]):
  1683.                     held_keys['size'] = 0
  1684.                    
  1685.                 elif update_yet:
  1686.                     length_exp = (max(length_increment,
  1687.                                      (pow(self.draw_data.length, length_exponential)
  1688.                                       - 1 / length_increment))
  1689.                                   * length_multiplier)
  1690.                     self.draw_data.length += length_exp * held_keys['size']
  1691.                     game_flags['recalculate'] = True
  1692.                     store_data['temp_fps'] = self.fps_smooth
  1693.  
  1694.                    
  1695.            
  1696.             #Update mouse information
  1697.             if game_flags['mouse_used'] or game_flags['recalculate']:
  1698.                 game_flags['recalculate'] = True
  1699.                 mouse_data = pygame.mouse.get_pos()
  1700.                 x, y = self.to_pygame(*mouse_data)
  1701.                 block_data['object'] = MouseToBlockID(x, y, self.draw_data)
  1702.                 block_data['id'] = block_data['object'].calculate()
  1703.                 block_data['taken'] = True
  1704.                 if block_data['id'] is not None and ai_turn is None:
  1705.                     block_data['taken'] = self.C3DObject.grid_data[block_data['id']] != ''
  1706.                    
  1707.            
  1708.             #If mouse was clicked
  1709.             if not game_flags['disable_background_clicks']:
  1710.                 if game_flags['clicked'] == 1 and not block_data['taken'] or ai_turn is not None:
  1711.                     store_data['waiting'] = (ai_turn if ai_turn is not None else block_data['id'], self.player)
  1712.                     store_data['waiting_start'] = time.time() + moving_wait
  1713.                     self._next_player()
  1714.                    
  1715.                    
  1716.             #Highlight square
  1717.             if not block_data['taken'] and not store_data['waiting'] and not game_data['overlay']:
  1718.                 self.C3DObject.grid_data[block_data['id']] = self.overlay_marker
  1719.                 game_flags['hover'] = block_data['id']
  1720.                
  1721.            
  1722.             #Recalculate the data to draw the grid
  1723.             if game_flags['recalculate']:
  1724.            
  1725.                 if not store_data['temp_fps']:
  1726.                     store_data['temp_fps'] = self.fps_main
  1727.                    
  1728.                 self.draw_data.segments = self.C3DObject.segments
  1729.                
  1730.                 self.draw_data.length = float(max((pow(1 / length_increment, 2) * self.draw_data.segments), self.draw_data.length, 2))
  1731.                 self.draw_data.angle = float(max(angle_increment, min(89, self.draw_data.angle, angle_max)))
  1732.                
  1733.                 self.draw_data._calculate()
  1734.                 if game_flags['reset']:
  1735.                     continue
  1736.                
  1737.             #Draw coloured squares
  1738.             for i in self.C3DObject.range_data:
  1739.                 if self.C3DObject.grid_data[i] != '':
  1740.                
  1741.                     chunk = i / self.C3DObject.segments_squared
  1742.                     coordinate = list(self.draw_data.relative_coordinates[i % self.C3DObject.segments_squared])
  1743.                     coordinate[1] -= chunk * self.draw_data.chunk_height
  1744.                    
  1745.                     square = [coordinate,
  1746.                               (coordinate[0] + self.draw_data.size_x_sm,
  1747.                                coordinate[1] - self.draw_data.size_y_sm),
  1748.                               (coordinate[0],
  1749.                                coordinate[1] - self.draw_data.size_y_sm * 2),
  1750.                               (coordinate[0] - self.draw_data.size_x_sm,
  1751.                                coordinate[1] - self.draw_data.size_y_sm),
  1752.                               coordinate]
  1753.  
  1754.                     #Player has mouse over square
  1755.                     block_colour = None
  1756.                     if self.C3DObject.grid_data[i] == self.overlay_marker:
  1757.                    
  1758.                        
  1759.                         if game_data['players'][self.player] is False:
  1760.                             block_colour = mix_colour(WHITE, WHITE, self.player_colours[self.player])
  1761.                    
  1762.                     #Square is taken by a player
  1763.                     else:
  1764.                         j = self.C3DObject.grid_data[i]
  1765.                        
  1766.                         #Square is being moved into, mix with red and white
  1767.                         mix = False
  1768.                         if isinstance(j, int) and j > 1:
  1769.                             j = 9 - j
  1770.                             moving_block = square
  1771.                             mix = True
  1772.                            
  1773.                         block_colour = self.player_colours[j]
  1774.                        
  1775.                         if mix:
  1776.                             block_colour = mix_colour(block_colour, GREY)
  1777.                    
  1778.                     if block_colour is not None:
  1779.                         pygame.draw.polygon(self.screen,
  1780.                                             block_colour,
  1781.                                             [self.to_canvas(*corner)
  1782.                                              for corner in square],
  1783.                                             0)
  1784.                                        
  1785.                
  1786.             #Draw grid
  1787.             for line in self.draw_data.line_coordinates:
  1788.                 pygame.draw.aaline(self.screen,
  1789.                                    BLACK,
  1790.                                    self.to_canvas(*line[0]),
  1791.                                    self.to_canvas(*line[1]),
  1792.                                    1)
  1793.            
  1794.            
  1795.             self._draw_score(game_flags['winner'])
  1796.            
  1797.            
  1798.             if game_data['debug']:
  1799.                 self._draw_debug(block_data)
  1800.            
  1801.             if game_data['overlay']:
  1802.            
  1803.                 store_data['temp_fps'] = self.fps_main
  1804.                 header_padding = self.padding[1] * 5
  1805.                 subheader_padding = self.padding[1] * 3
  1806.                 self.blit_list = []
  1807.                 self.rect_list = []
  1808.                 self.screen.blit(self.backdrop, (0, 0))
  1809.                 screen_width_offset = (self.width - self.overlay_width) / 2
  1810.                
  1811.                 current_height = header_padding + self.padding[1]
  1812.                
  1813.                 #Set page titles
  1814.                 if game_data['overlay'] == 'instructions':
  1815.                     title_message = 'Instructions/About'
  1816.                     subtitle_message = ''
  1817.                 elif game_data['move_number'] + bool(store_data['waiting']) and game_data['overlay'] == 'options':
  1818.                     title_message = 'Options'
  1819.                     subtitle_message = ''
  1820.                 else:
  1821.                     title_message = 'Connect 3D'
  1822.                     subtitle_message = 'By Peter Hunt'
  1823.                    
  1824.                 title_text = self.font_lg.render(title_message, 1, BLACK)
  1825.                 title_size = title_text.get_rect()[2:]
  1826.                 self.blit_list.append((title_text, (self.padding[0] + screen_width_offset, current_height)))
  1827.                
  1828.                 current_height += self.padding[1] + title_size[1]
  1829.                
  1830.                 subtitle_text = self.font_md.render(subtitle_message, 1, BLACK)
  1831.                 subtitle_size = subtitle_text.get_rect()[2:]
  1832.                 self.blit_list.append((subtitle_text, (self.padding[0] + screen_width_offset, current_height)))
  1833.                
  1834.                 current_height += subtitle_size[1]
  1835.                 if subtitle_message:
  1836.                     current_height += header_padding
  1837.                    
  1838.                
  1839.                 if game_data['overlay'] == 'options':
  1840.                    
  1841.                    
  1842.                     #Player options
  1843.                     players_unsaved = [p1, p2]
  1844.                     players_original = list(game_data['players'])
  1845.                     player_hover = store_data['player_hover']
  1846.                     store_data['player_hover'] = None
  1847.                     options = ['Human', 'Beginner', 'Easy', 'Medium', 'Hard', 'Extreme']
  1848.                    
  1849.                     for player in range(len(game_data['players'])):
  1850.                         if players_unsaved[player] is False:
  1851.                             players_unsaved[player] = -1
  1852.                         else:
  1853.                             players_unsaved[player] = get_bot_difficulty(players_unsaved[player], _debug=True)
  1854.                         if players_original[player] is False:
  1855.                             players_original[player] = -1
  1856.                         else:
  1857.                             players_original[player] = get_bot_difficulty(players_original[player], _debug=True)
  1858.                            
  1859.                         params = []
  1860.                         for i in range(len(options)):
  1861.                             params.append([i == players_unsaved[player] or players_unsaved[player] < 0 and not i,
  1862.                                            i == players_original[player] or players_original[player] < 0 and not i,
  1863.                                            [player, i] == player_hover])
  1864.                        
  1865.                         option_data = self._draw_options('Player {}: '.format(player),
  1866.                                                          options,
  1867.                                                          params,
  1868.                                                          screen_width_offset,
  1869.                                                          current_height)
  1870.                        
  1871.                         selected_option, options_size = option_data
  1872.                        
  1873.                         current_height += options_size
  1874.                         if not player:
  1875.                             current_height += self.padding[1]
  1876.                         else:
  1877.                             current_height += subheader_padding
  1878.                        
  1879.                         #Calculate mouse info
  1880.                         if selected_option is not None:
  1881.                             player_set = selected_option - 1
  1882.                             if player_set < 0:
  1883.                                 player_set = False
  1884.                             store_data['player_hover'] = [player, selected_option]
  1885.                             if game_flags['clicked']:
  1886.                                 if not player:
  1887.                                     p1 = player_set
  1888.                                 else:
  1889.                                     p2 = player_set
  1890.                                 if not game_data['move_number']:
  1891.                                     game_data['players'] = (p1, p2)  
  1892.                                  
  1893.                    
  1894.                     #Ask whether to flip the grid
  1895.                     options = ['Yes', 'No']
  1896.                     params = []
  1897.                     for i in range(len(options)):
  1898.                         params.append([not i and allow_shuffle or i and not allow_shuffle,
  1899.                                        not i and game_data['shuffle'][0] or i and not game_data['shuffle'][0],
  1900.                                        not i and store_data['shuffle_hover'] or i and not store_data['shuffle_hover'] and store_data['shuffle_hover'] is not None])
  1901.                     option_data = self._draw_options('Flip grid every 3 goes? ',
  1902.                                                      ['Yes', 'No'],
  1903.                                                      params,
  1904.                                                      screen_width_offset,
  1905.                                                      current_height)
  1906.                                                      
  1907.                     selected_option, options_size = option_data
  1908.                     current_height += subheader_padding + options_size
  1909.                    
  1910.                     #Calculate mouse info
  1911.                     store_data['shuffle_hover'] = None
  1912.                     if selected_option is not None:
  1913.                         store_data['shuffle_hover'] = not selected_option
  1914.                         if game_flags['clicked']:
  1915.                             allow_shuffle = not selected_option
  1916.                             if not game_data['move_number']:
  1917.                                 game_data['shuffle'][0] = allow_shuffle
  1918.                    
  1919.                     #Toggle hidden debug option with ctrl+alt+d
  1920.                     if not (not key[pygame.K_d]
  1921.                             or not (key[pygame.K_RCTRL] or key[pygame.K_LCTRL])
  1922.                             or not (key[pygame.K_RALT] or key[pygame.K_LALT])):
  1923.                            
  1924.                         store_data['debug_hover']
  1925.                         options = ['Yes', 'No']
  1926.                         params = []
  1927.                         for i in range(len(options)):
  1928.                             params.append([not i and game_data['debug'] or i and not game_data['debug'],
  1929.                                            not i and game_data['debug'] or i and not game_data['debug'],
  1930.                                            not i and store_data['debug_hover'] or i and not store_data['debug_hover'] and store_data['debug_hover'] is not None])
  1931.                        
  1932.                         option_data = self._draw_options('Show debug info? ',
  1933.                                                          ['Yes', 'No'],
  1934.                                                          params,
  1935.                                                          screen_width_offset,
  1936.                                                          current_height)
  1937.                                                  
  1938.                         selected_option, options_size = option_data
  1939.                        
  1940.                         store_data['debug_hover'] = None
  1941.                         if selected_option is not None:
  1942.                             store_data['debug_hover'] = not selected_option
  1943.                             if game_flags['clicked']:
  1944.                                 game_data['debug'] = not selected_option
  1945.                        
  1946.                         current_height += subheader_padding + options_size
  1947.                    
  1948.                                                  
  1949.                     box_spacing = (header_padding + self.padding[1]) if game_data['move_number'] else (self.padding[1] + self.font_lg_size)
  1950.                    
  1951.                     box_height = [current_height]
  1952.                    
  1953.                     #Tell to restart game
  1954.                     if game_data['move_number']:
  1955.                         current_height += box_spacing
  1956.                         restart_message = 'Restart game to apply settings.'
  1957.                         restart_text = self.font_md.render(restart_message, 1, BLACK)
  1958.                         restart_size = restart_text.get_rect()[2:]
  1959.                         self.blit_list.append((restart_text, ((self.width - restart_size[0]) / 2, current_height)))
  1960.                         current_height += header_padding
  1961.                        
  1962.                         #Continue button
  1963.                         if self._pygame_button('Continue',
  1964.                                                store_data['continue'],
  1965.                                                current_height,
  1966.                                                -1):
  1967.                             store_data['continue'] = True
  1968.                             if game_flags['clicked']:
  1969.                                 game_data['overlay'] = None
  1970.                         else:
  1971.                             store_data['continue'] = False
  1972.                            
  1973.                    
  1974.                     box_height.append(current_height)
  1975.                     current_height += box_spacing
  1976.                    
  1977.                     #Instructions button
  1978.                     if self._pygame_button('Instructions' if game_data['move_number'] else 'Help',
  1979.                                            store_data['instructions'],
  1980.                                            box_height[0],
  1981.                                            0 if game_data['move_number'] else 1):
  1982.                         store_data['instructions'] = True
  1983.                         if game_flags['clicked']:
  1984.                             game_data['overlay'] = 'instructions'
  1985.                     else:
  1986.                         store_data['instructions'] = False
  1987.                    
  1988.                     #New game button
  1989.                     if self._pygame_button('New Game' if game_data['move_number'] else 'Start',
  1990.                                            store_data['new_game'],
  1991.                                            box_height[bool(game_data['move_number'])],
  1992.                                            bool(game_data['move_number']) if game_data['move_number'] else -1):
  1993.                         store_data['new_game'] = True
  1994.                         if game_flags['clicked']:
  1995.                             game_flags['reset'] = True
  1996.                             game_data['overlay'] = None
  1997.                     else:
  1998.                         store_data['new_game'] = False
  1999.                                                
  2000.                        
  2001.                    
  2002.                     #Quit button
  2003.                     if self._pygame_button('Quit to Desktop' if game_data['move_number'] else 'Quit',
  2004.                                            store_data['exit'],
  2005.                                            current_height):
  2006.                         store_data['exit'] = True
  2007.                         if game_flags['clicked']:
  2008.                             game_flags['quit'] = True
  2009.                     else:
  2010.                         store_data['exit'] = False
  2011.                        
  2012.                 #Draw background
  2013.                 background_square = (screen_width_offset, header_padding, self.overlay_width, current_height + self.padding[1] * 2)
  2014.                 pygame.draw.rect(self.screen, WHITE, background_square, 0)
  2015.                 pygame.draw.rect(self.screen, BLACK, background_square, 1)
  2016.                
  2017.                 for i in self.rect_list:
  2018.                     rect_data = [self.screen] + i
  2019.                     pygame.draw.rect(*rect_data)
  2020.                
  2021.                 for i in self.blit_list:
  2022.                     self.screen.blit(*i)
  2023.            
  2024.            
  2025.             pygame.display.flip()
  2026.                        
  2027.     def _pygame_button(self, message, hover, current_height, width_multipler=0):
  2028.                        
  2029.         multiplier = 3
  2030.        
  2031.         #Set up text
  2032.         text_colour = BLACK if hover else GREY
  2033.         text_object = self.font_lg.render(message, 1, text_colour)
  2034.         text_size = text_object.get_rect()[2:]
  2035.        
  2036.        
  2037.         centre_offset = self.width / 10 * width_multipler
  2038.         text_x = (self.width - text_size[0]) / 2
  2039.         if width_multipler > 0:
  2040.             text_x += text_size[0] / 2
  2041.         if width_multipler < 0:
  2042.             text_x -= text_size[0] / 2
  2043.         text_x += centre_offset
  2044.        
  2045.        
  2046.         text_square = (text_x - self.option_padding * (multiplier + 1),
  2047.                        current_height - self.option_padding * multiplier,
  2048.                        text_size[0] + self.option_padding * (2 * multiplier + 2),
  2049.                        text_size[1] + self.option_padding * (2 * multiplier - 1))
  2050.    
  2051.         self.blit_list.append((text_object, (text_x, current_height)))
  2052.        
  2053.         #Detect if mouse is over it
  2054.         x, y = pygame.mouse.get_pos()
  2055.         in_x = text_square[0] < x < text_square[0] + text_square[2]
  2056.         in_y = text_square[1] < y < text_square[1] + text_square[3]
  2057.            
  2058.         if in_x and in_y:
  2059.             return True
  2060.                
  2061.         return False
  2062.        
  2063.        
  2064.     def _draw_options(self, message, options, params, screen_width_offset, current_height):
  2065.         """Draw a list of options and check for inputs.
  2066.        
  2067.        Parameters:
  2068.            message (str): Text to display next to the options.
  2069.            
  2070.            options (list): Names of the options.
  2071.            
  2072.            params (list): Contains information on the options.
  2073.                It needs to have the same amount of records as
  2074.                options, with each of these being a list of 3 items.
  2075.                These are used to colour the text in the correct
  2076.                way.
  2077.                
  2078.                param[option][0] = new selection
  2079.                param[option][1] = currently active
  2080.                param[option][2] = mouse hoving over
  2081.            
  2082.            screen_width_offset (int): The X position to draw the
  2083.                text.
  2084.            
  2085.            current_height (int/float): The Y position to draw the
  2086.                text.
  2087.        """
  2088.         message_text = self.font_md.render(message, 1, BLACK)
  2089.         message_size = message_text.get_rect()[2:]
  2090.         self.blit_list.append((message_text, (self.padding[0] + screen_width_offset, current_height)))
  2091.        
  2092.         option_text = [self.font_md.render(i, 1, BLACK) for i in options]
  2093.         option_size = [i.get_rect()[2:] for i in option_text]
  2094.         option_square_list = []
  2095.    
  2096.         for i in range(len(options)):
  2097.             width_offset = (sum(j[0] + 2 for j in option_size[:i])
  2098.                             + self.padding[0] * (i + 1) #gap between the start
  2099.                             + message_size[0] + screen_width_offset)
  2100.  
  2101.             option_square = (width_offset - self.option_padding,
  2102.                              current_height - self.option_padding,
  2103.                              option_size[i][0] + self.option_padding * 2,
  2104.                              option_size[i][1] + self.option_padding)
  2105.             option_square_list.append(option_square)
  2106.            
  2107.            
  2108.             #Set colours
  2109.             option_colours = list(SELECTION['Default'])
  2110.             param_order = ('Waiting', 'Selected', 'Hover')
  2111.             for j in range(len(params[i])):
  2112.                 if params[i][j]:
  2113.                     rect_colour, text_colour = list(SELECTION[param_order[j]])
  2114.                     if rect_colour is not None:
  2115.                         option_colours[0] = rect_colour
  2116.                     if text_colour is not None:
  2117.                         option_colours[1] = text_colour
  2118.                    
  2119.             rect_colour, text_colour = option_colours
  2120.            
  2121.             self.rect_list.append([rect_colour, option_square])
  2122.             self.blit_list.append((self.font_md.render(options[i], 1, text_colour), (width_offset, current_height)))
  2123.        
  2124.         x, y = pygame.mouse.get_pos()
  2125.         selected_square = None
  2126.         for square in range(len(option_square_list)):
  2127.             option_square = option_square_list[square]
  2128.             in_x = option_square[0] < x < option_square[0] + option_square[2]
  2129.             in_y = option_square[1] < y < option_square[1] + option_square[3]
  2130.             if in_x and in_y:
  2131.                 selected_square = square
  2132.                
  2133.         return (selected_square, message_size[1])
  2134.        
  2135.            
  2136.     def _format_output(self, text):
  2137.         """Format text to remove invalid characters."""
  2138.         left_bracket = ('[', '{')
  2139.         right_bracket = (']', '}')
  2140.         for i in left_bracket:
  2141.             text = text.replace(i, '(')
  2142.         for i in right_bracket:
  2143.             text = text.replace(i, ')')
  2144.         return text
  2145.    
  2146.     def _draw_score(self, winner):
  2147.         """Draw the title."""
  2148.        
  2149.         #Format scores
  2150.         point_marker = '/'
  2151.         p0_points = self.C3DObject.current_points[0]
  2152.         p1_points = self.C3DObject.current_points[1]
  2153.        
  2154.         p0_font_top = self.font_md.render('Player 0', 1,  BLACK, self.player_colours[0])
  2155.         p1_font_top = self.font_md.render('Player 1', 1, BLACK, self.player_colours[1])
  2156.         p0_font_bottom = self.font_lg.render(point_marker * p0_points, 1,  BLACK)
  2157.         p1_font_bottom = self.font_lg.render(point_marker * p1_points, 1,  BLACK)
  2158.        
  2159.         p_size_top = p1_font_top.get_rect()[2:]
  2160.         p_size_bottom = p1_font_bottom.get_rect()[2:]
  2161.        
  2162.         if winner is None:
  2163.             go_message = "Player {}'s turn!".format(self.player)
  2164.         else:
  2165.             if len(winner) != 1:
  2166.                 go_message = 'The game was a draw!'
  2167.             else:
  2168.                 go_message = 'Player {} won!'.format(winner[0])
  2169.            
  2170.         go_font = self.font_lg.render(go_message, 1, BLACK)
  2171.         go_size = go_font.get_rect()[2:]
  2172.        
  2173.         self.screen.blit(go_font, ((self.width - go_size[0]) / 2, self.padding[1] * 3))
  2174.         self.screen.blit(p0_font_top, (self.padding[0], self.padding[1]))
  2175.         self.screen.blit(p1_font_top, (self.width - p_size_top[0] - self.padding[0], self.padding[1]))
  2176.         self.screen.blit(p0_font_bottom, (self.padding[0], self.padding[1] + p_size_top[1]))
  2177.         self.screen.blit(p1_font_bottom, (self.width - p_size_bottom[0] - self.padding[0], self.padding[1] + p_size_top[1]))
  2178.  
  2179.    
  2180.     def _draw_debug(self, block_data):
  2181.         """Show the debug information."""
  2182.    
  2183.         mouse_data = pygame.mouse.get_pos()
  2184.         x, y = self.to_pygame(*mouse_data)
  2185.        
  2186.         debug_coordinates = block_data['object'].calculate(debug=1)
  2187.         if debug_coordinates is not None:
  2188.             if all(i is not None for i in debug_coordinates):
  2189.                 pygame.draw.aaline(self.screen,
  2190.                             RED,
  2191.                             pygame.mouse.get_pos(),
  2192.                             self.to_canvas(*debug_coordinates[1]),
  2193.                             1)
  2194.                 pygame.draw.line(self.screen,
  2195.                             RED,
  2196.                             self.to_canvas(*debug_coordinates[0]),
  2197.                             self.to_canvas(*debug_coordinates[1]),
  2198.                             2)
  2199.    
  2200.         possible_blocks = block_data['object'].find_overlap()
  2201.        
  2202.         y_mult = str(block_data['object'].y_coordinate * self.C3DObject.segments_squared)
  2203.         if y_mult[0] != '-':
  2204.             y_mult = '+{}'.format(y_mult)
  2205.         info = ['DEBUG INFO',
  2206.                 'FPS: {}'.format(int(round(self.clock.get_fps(), 0))),
  2207.                 'Segments: {}'.format(self.C3DObject.segments),
  2208.                 'Angle: {}'.format(self.draw_data.angle),
  2209.                 'Side length: {}'.format(self.draw_data.length),
  2210.                 'Coordinates: {}'.format(mouse_data),
  2211.                 'Chunk: {}'.format((block_data['object'].width,
  2212.                                     block_data['object'].height,
  2213.                                     block_data['object'].y_coordinate)),
  2214.                 'X Slice: {}'.format(block_data['object'].find_x_slice()),
  2215.                 'Y Slice: {}'.format(block_data['object'].find_y_slice()),
  2216.                 'Possible blocks: {} {}'.format(possible_blocks, y_mult),
  2217.                 'Block weight: {}'.format(block_data['object'].calculate(debug=2)),
  2218.                 'Block ID: {}'.format(block_data['object'].calculate())]
  2219.                
  2220.                
  2221.         font_render = [self.font_sm.render(self._format_output(i), 1, BLACK) for i in info]
  2222.         font_size = [i.get_rect()[2:] for i in font_render]
  2223.         for i in range(len(info)):
  2224.             message_height = self.height - sum(j[1] for j in font_size[i:])
  2225.             self.screen.blit(font_render[i], (0, message_height))
  2226.            
  2227.        
  2228.        
  2229.         #Format the AI text output
  2230.         ai_message = []
  2231.         for i in self.C3DObject.ai_message:
  2232.        
  2233.             #Split into chunks of 50 if longer
  2234.             message_len = len(i)
  2235.             message = [self._format_output(i[n * 50:(n + 1) * 50]) for n in range(round_up(message_len / 50.0))]
  2236.             ai_message += message
  2237.        
  2238.         font_render = [self.font_sm.render(i, 1, BLACK) for i in ai_message]
  2239.         font_size = [i.get_rect()[2:] for i in font_render]
  2240.  
  2241.         for i in range(len(ai_message)):
  2242.             message_height = self.height - sum(j[1] for j in font_size[i:])
  2243.             self.screen.blit(font_render[i], (self.width - font_size[i][0], message_height))
  2244.    
  2245. def round_up(x):
  2246.     return int(x) + bool(x % 1)
  2247.  
  2248.  
  2249. if __name__ == '__main__':
  2250.     Connect3D().play()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement