Advertisement
Guest User

Untitled

a guest
Sep 4th, 2015
80
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 30.72 KB | None | 0 0
  1. Player X: //// Player O: /////
  2. ________________
  3. / X / / / O /|
  4. /___/___/___/___/ |
  5. / O / X / / X / |
  6. /___/___/___/___/ |
  7. / / O / X / O / |
  8. /___/___/___/___/ |
  9. / / / / O / |
  10. /___/___/___/___/ |
  11. | |________|_______|
  12. | / / /|O / /|
  13. | /___/___/_|_/___/ |
  14. | / X / X / O|/ X / |
  15. | /___/___/___|___/ |
  16. | / / O / O /| / |
  17. | /___/___/___/_|_/ |
  18. | / / / O / |/ |
  19. |/___/___/___/___| |
  20. | |________|_______|
  21. | / X / O /| / /|
  22. | /___/___/_|_/___/ |
  23. | / X / X / O|/ / |
  24. | /___/___/___|___/ |
  25. | / / O / X /| / |
  26. | /___/___/___/_|_/ |
  27. | / / / / X|/ |
  28. |/___/___/___/___| |
  29. | |________|_______|
  30. | / O / /| / /
  31. | /___/___/_|_/___/
  32. | / X / X / X|/ X /
  33. | /___/___/___|___/
  34. | / O / O / /| /
  35. | /___/___/___/_|_/
  36. | / O / / / X|/
  37. |/___/___/___/___|
  38. Player O's turn...
  39.  
  40. Connect3D(_raw_data='X OOX X OXO O O XXOX OO O XO XXO OX XO XXXXOO O X.1').play()
  41.  
  42. import itertools
  43. import operator
  44. import random
  45. from collections import defaultdict
  46.  
  47. class Connect3D(object):
  48. """Class to store the Connect3D game data.
  49. The data is stored in a 1D list, but is converted to a 3D representation for the user.
  50. """
  51. player_symbols = 'XO'
  52. grid_size_recommended = 4
  53.  
  54. def __init__(self, grid_size=grid_size_recommended, _raw_data=None):
  55. """Set up the grid and which player goes first.
  56.  
  57. Parameters:
  58. grid_size (int): How long each side of the grid should be.
  59. The game works best with even numbers, 4 is recommended.
  60.  
  61. _raw_data (str or None, optional): Passed in from __repr__,
  62. contains the grid data and current player.
  63. Format: "joined(grid_data).current_player"
  64. """
  65.  
  66. self.current_player = random.randint(0, 1)
  67.  
  68. #Read from _raw_data
  69. if _raw_data is not None:
  70. split_data = _raw_data.split('.')
  71. self.grid_data = list(i if i != ' ' else '' for i in split_data[0])
  72. self.grid_size = calculate_grid_size(self.grid_data)
  73. if len(self.grid_data) != pow(self.grid_size, 3):
  74. self.grid_data = self.grid_data[:pow(self.grid_size, 3)]
  75.  
  76. if len(split_data) > 1:
  77. self.current_player = int(int(split_data[1]))
  78.  
  79. #Set up class
  80. else:
  81. try:
  82. self.grid_size = int(grid_size)
  83. except TypeError:
  84. raise TypeError('grid_size must be an integer')
  85. self.grid_data = ['' for i in range(pow(grid_size, 3))]
  86.  
  87.  
  88. self.grid_size_squared = pow(self.grid_size, 2)
  89.  
  90. #Calculate the edge numbers for each direction
  91. self.direction_edges = {}
  92. self.direction_edges['U'] = range(self.grid_size_squared)
  93. self.direction_edges['D'] = range(self.grid_size_squared*(self.grid_size-1), self.grid_size_squared*self.grid_size)
  94. self.direction_edges['R'] = [i*self.grid_size+self.grid_size-1 for i in range(self.grid_size_squared)]
  95. self.direction_edges['L'] = [i*self.grid_size for i in range(self.grid_size_squared)]
  96. self.direction_edges['F'] = [i*self.grid_size_squared+j+self.grid_size_squared-self.grid_size for i in range(self.grid_size) for j in range(self.grid_size)]
  97. self.direction_edges['B'] = [i*self.grid_size_squared+j for i in range(self.grid_size) for j in range(self.grid_size)]
  98. self.direction_edges[' '] = []
  99.  
  100. #Calculate the addition needed to move in each direction
  101. self.direction_maths = {}
  102. self.direction_maths['D'] = self.grid_size_squared
  103. self.direction_maths['R'] = 1
  104. self.direction_maths['F'] = self.grid_size
  105. self.direction_maths['U'] = -self.direction_maths['D']
  106. self.direction_maths['L'] = -self.direction_maths['R']
  107. self.direction_maths['B'] = -self.direction_maths['F']
  108. self.direction_maths[' '] = 0
  109.  
  110.  
  111. def __repr__(self):
  112. """Format the data to allow it to be imported again as a new object."""
  113. grid_data_joined = ''.join(str(i).ljust(1) for i in self.grid_data)
  114. return "Connect3D.from_string('{}.{}')".format(grid_data_joined, self.current_player)
  115.  
  116. def __str__(self):
  117. """Use the grid_data to output a grid of the correct size.
  118. Each value in grid_data must be 1 character or formatting will be wrong.
  119.  
  120. >>> grid_data = range(8)
  121.  
  122. >>> print Connect3D(_raw_data=''.join(str(i) if i != '' else ' ' for i in grid_data))
  123. ________
  124. / 0 / 1 /|
  125. /___/___/ |
  126. / 2 / 3 / |
  127. /___/___/ |
  128. | |____|___|
  129. | / 4 /|5 /
  130. | /___/_|_/
  131. | / 6 / 7|/
  132. |/___/___|
  133.  
  134. """
  135. k = 0
  136.  
  137. grid_range = range(self.grid_size)
  138. grid_output = []
  139. for j in grid_range:
  140.  
  141. row_top = ' '*(self.grid_size*2+1) + '_'*(self.grid_size*4)
  142. if j:
  143. row_top = '|' + row_top[:self.grid_size*2-1] + '|' + '_'*(self.grid_size*2) + '|' + '_'*(self.grid_size*2-1) + '|'
  144. grid_output.append(row_top)
  145.  
  146. for i in grid_range:
  147. row_display = ' '*(self.grid_size*2-i*2) + '/' + ''.join((' ' + str(self.grid_data[k+x]).ljust(1) + ' /') for x in grid_range)
  148. k += self.grid_size
  149. row_bottom = ' '*(self.grid_size*2-i*2-1) + '/' + '___/'*self.grid_size
  150.  
  151. if j != grid_range[-1]:
  152. row_display += ' '*(i*2) + '|'
  153. row_bottom += ' '*(i*2+1) + '|'
  154. if j:
  155. row_display = row_display[:self.grid_size*4+1] + '|' + row_display[self.grid_size*4+2:]
  156. row_bottom = row_bottom[:self.grid_size*4+1] + '|' + row_bottom[self.grid_size*4+2:]
  157.  
  158. row_display = '|' + row_display[1:]
  159. row_bottom = '|' + row_bottom[1:]
  160.  
  161. grid_output += [row_display, row_bottom]
  162.  
  163. return 'n'.join(grid_output)
  164.  
  165. def _get_winning_player(self):
  166. """Return a list of the player(s) with the highest points.
  167.  
  168. >>> C3D = Connect3D()
  169. >>> C3D.update_score()
  170.  
  171. When X has a higher score.
  172. >>> C3D.current_points['X'] = 5
  173. >>> C3D.current_points['O'] = 1
  174. >>> C3D._get_winning_player()
  175. ['X']
  176.  
  177. When both scores are the same.
  178. >>> C3D.current_points['O'] = 5
  179. >>> C3D._get_winning_player()
  180. ['O', 'X']
  181.  
  182. When there are no winners.
  183. >>> C3D = Connect3D()
  184. >>> C3D.update_score()
  185. >>> C3D._get_winning_player()
  186. []
  187. """
  188. self.update_score()
  189. return get_max_dict_keys(self.current_points)
  190.  
  191.  
  192. def play(self, multiplayer=False, grid_shuffle_chance=None):
  193. """Start or continue a game.
  194.  
  195. Parameters:
  196. multiplayer (bool): If the AI should be swapped with another human player.
  197. Default: False
  198.  
  199. grid_shuffle_chance (int, float or None, optional): Percentage chance to shuffle
  200. the grid after each turn.
  201. Reverts to the default chance if left as None.
  202. """
  203.  
  204. computer_player = True
  205. self.current_player = int(not self.current_player)
  206.  
  207. #Game loop
  208. while True:
  209.  
  210. #Switch current player
  211. self.current_player = int(not self.current_player)
  212.  
  213. self.update_score()
  214. self.show_score()
  215. print self
  216. was_flipped = self.shuffle(chance=grid_shuffle_chance)
  217. if was_flipped:
  218. print "Grid was flipped!"
  219.  
  220. #Check if no spaces are left
  221. if '' not in self.grid_data:
  222. winning_player = self._get_winning_player()
  223. if len(winning_player) == 1:
  224. print 'Player {} won!'.format(winning_player[0])
  225. else:
  226. print 'The game was a draw!'
  227.  
  228. #Ask to play again and check if answer is a variant of 'yes' or 'ok'
  229. print 'Play again?'
  230. play_again = raw_input().lower()
  231. if any(i in play_again for i in ('y', 'k')):
  232. self.reset()
  233. else:
  234. return
  235. break
  236.  
  237.  
  238. #Player takes a move, function returns True if it updates the grid, otherwise loop again
  239. print "Player {}'s turn...".format(self.player_symbols[self.current_player])
  240. if not self.current_player or multiplayer:
  241. while not self.make_move(self.player_symbols[self.current_player], raw_input().replace(',', ' ').replace('.', ' ').split()):
  242. print "Grid cell is not available, try again."
  243. else:
  244. ai_go = SimpleC3DAI(self, self.current_player).calculate_next_move()
  245. if not self.make_move(self.player_symbols[self.current_player], ai_go):
  246. print "Something unknown went wrong with the AI, apologies if it affected the game."
  247. else:
  248. print "AI moved to point {}.".format(PointConversion(self.grid_size, ai_go).to_3d())
  249.  
  250. def make_move(self, id, *args):
  251. """Update the grid data with a new move.
  252.  
  253. Parameters:
  254. id (str): Character to write into the grid.
  255.  
  256. args (int, tuple or list): Where in the grid to place the ID.
  257. Can be input as an integer (grid cell number), 3 integers,
  258. a tuple or list (3D coordinates)
  259.  
  260. >>> C3D = Connect3D(2)
  261.  
  262. >>> C3D.make_move('a', 1)
  263. True
  264. >>> C3D.make_move('b', 1)
  265. False
  266. >>> C3D.make_move('c', -1)
  267. False
  268. >>> C3D.make_move('d', 2, 2, 2)
  269. True
  270. >>> C3D.make_move('e', [1, 1, 2])
  271. True
  272. >>> C3D.make_move('f', (1, 1, 3))
  273. False
  274.  
  275. >>> C3D.grid_data
  276. ['', 'a', '', '', 'e', '', '', 'd']
  277. >>> print C3D
  278. ________
  279. / / a /|
  280. /___/___/ |
  281. / / / |
  282. /___/___/ |
  283. | |____|___|
  284. | / e /| /
  285. | /___/_|_/
  286. | / / d|/
  287. |/___/___|
  288. """
  289.  
  290. #Convert points to the grid cell ID
  291. if len(args) == 1:
  292. if not str(args[0]).replace('-','').isdigit():
  293. if len(args[0]) == 1:
  294. try:
  295. i = int(args[0][0])
  296. except ValueError:
  297. return False
  298. else:
  299. i = PointConversion(self.grid_size, args[0]).to_int()
  300. else:
  301. i = int(args[0])
  302. else:
  303. i = PointConversion(self.grid_size, tuple(args)).to_int()
  304.  
  305. #Add to grid if cell is empty
  306. if 0 <= i <len(self.grid_data) and not self.grid_data[i] and i is not None:
  307. self.grid_data[i] = id
  308. return True
  309. else:
  310. return False
  311.  
  312.  
  313. def shuffle(self, chance=None, second_chance=None, repeats=None, no_shuffle=[]):
  314. """Mirror the grid in the X, Y, or Z axis.
  315.  
  316. Each time one of the directions is flipped, there is a 50% chance of it happening again.
  317. This means it has the same overall chance to flip, but it is not limited to a single axis.
  318.  
  319. Parameters:
  320. chance:
  321. Percent chance of a flip happening.
  322. Default: 10
  323. Type: int/float
  324.  
  325. second_chance:
  326. Percent chance of subsequent flips happening after the first.
  327. Default: 50
  328. Type: int/float
  329.  
  330. repeats:
  331. Number of attempts to flip at the above chance.
  332. Default: 3
  333. Type: int
  334.  
  335. no_shuffle:
  336. List of directions already flipped so it won't reverse anything.
  337. Type: list
  338. """
  339. #Set defaults
  340. if chance is None:
  341. chance = 10
  342. if second_chance is None:
  343. second_chance = 50
  344. if repeats is None:
  345. repeats = 3
  346.  
  347. #Calculate range of random numbers
  348. chance = min(100, chance)
  349. if chance > 0:
  350. chance = int(round(400/chance))-1
  351.  
  352. #Attempt to flip grid
  353. for i in range(repeats):
  354. shuffle_num = random.randint(0, chance)
  355. if shuffle_num in (0, 1, 2, 3) and shuffle_num not in no_shuffle:
  356. no_shuffle.append(shuffle_num)
  357. if shuffle_num == 0:
  358. self.grid_data = SwapGridData(self.grid_data).x()
  359. if shuffle_num == 1:
  360. self.grid_data = SwapGridData(self.grid_data).y()
  361. if shuffle_num == 2:
  362. self.grid_data = SwapGridData(self.grid_data).z()
  363. if shuffle_num == 3:
  364. self.grid_data = SwapGridData(self.grid_data).reverse()
  365. if self.shuffle(chance=second_chance, no_shuffle=no_shuffle) or not not no_shuffle:
  366. return True
  367.  
  368.  
  369. def update_score(self):
  370. """Recalculate the score.
  371.  
  372. There are 26 total directions from each point, or 13 lines, calculated in
  373. the DirectionCalculation() class. For each of the 13 lines, look both ways
  374. and count the number of values that match the current player.
  375.  
  376. This will find any matches from one point, so it's simple to then iterate
  377. through every point. A hash of each line is stored to avoid duplicates.
  378. """
  379.  
  380. try:
  381. self.grid_data_last_updated
  382. except AttributeError:
  383. self.grid_data_last_updated = None
  384.  
  385. if self.grid_data_last_updated != hash(tuple(self.grid_data)):
  386.  
  387. #Store hash of grid_data in it's current state to avoid unnecessarily running the code again when there's been no changes
  388. self.grid_data_last_updated = hash(tuple(self.grid_data))
  389.  
  390.  
  391. self.current_points = defaultdict(int)
  392. all_matches = set()
  393.  
  394. #Loop through each point
  395. for starting_point in range(len(self.grid_data)):
  396.  
  397. current_player = self.grid_data[starting_point]
  398.  
  399. if current_player:
  400.  
  401. for i in DirectionCalculation().opposite_direction:
  402.  
  403. #Get a list of directions and calculate movement amount
  404. possible_directions = [list(i)]
  405. possible_directions += [[j.replace(i, '') for i in possible_directions[0] for j in DirectionCalculation().direction_group.values() if i in j]]
  406. direction_movement = sum(self.direction_maths[j] for j in possible_directions[0])
  407.  
  408. #Build list of invalid directions
  409. invalid_directions = [[self.direction_edges[j] for j in possible_directions[k]] for k in (0, 1)]
  410. invalid_directions = [join_list(j) for j in invalid_directions]
  411.  
  412. num_matches = 1
  413. list_match = [starting_point]
  414.  
  415. #Use two loops for the opposite directions
  416. for j in (0, 1):
  417.  
  418. current_point = starting_point
  419.  
  420. while current_point not in invalid_directions[j] and 0 < current_point < len(self.grid_data):
  421. current_point += direction_movement * int('-'[:j] + '1')
  422. if self.grid_data[current_point] == current_player:
  423. num_matches += 1
  424. list_match.append(current_point)
  425. else:
  426. break
  427.  
  428. #Add a point if enough matches
  429. if num_matches == self.grid_size:
  430.  
  431. list_match = hash(tuple(sorted(list_match)))
  432. if list_match not in all_matches:
  433. all_matches.add(list_match)
  434. self.current_points[current_player] += 1
  435.  
  436.  
  437. def show_score(self, digits=False, marker='/'):
  438. """Print the current points.
  439.  
  440. Parameters:
  441. digits (bool, optional): If the score should be output as a number,
  442. or as individual marks.
  443.  
  444. marker (str, optional): How each point should be displayed if
  445. digits are not being used.
  446.  
  447. >>> C3D = Connect3D()
  448. >>> C3D.update_score()
  449. >>> C3D.current_points['X'] = 5
  450. >>> C3D.current_points['O'] = 1
  451.  
  452. >>> C3D.show_score(False, '/')
  453. Player X: ///// Player O: /
  454. >>> C3D.show_score(True)
  455. Player X: 5 Player O: 1
  456. """
  457. self.update_score()
  458. multiply_value = 1 if digits else marker
  459. print 'Player X: {x} Player O: {o}'.format(x=multiply_value*(self.current_points['X']), o=multiply_value*self.current_points['O'])
  460.  
  461.  
  462. def reset(self):
  463. """Empty the grid without creating a new Connect3D object."""
  464. self.grid_data = ['' for i in range(pow(self.grid_size, 3))]
  465.  
  466.  
  467. class DirectionCalculation(object):
  468. """Calculate which directions are possible to move in, based on the 6 directions.
  469. Any combination is fine, as long as it doesn't go back on itself, hence why X, Y
  470. and Z have been given two values each, as opposed to just using six values.
  471.  
  472. Because the code to calculate score will look in one direction then reverse it,
  473. the list then needs to be trimmed down to remove any duplicate directions (eg.
  474. up/down and upright/downleft are both duplicates)
  475.  
  476. The code will output the following results, it is possible to use these instead of the class.
  477. direction_group = {'Y': 'UD', 'X': 'LR', 'Z': 'FB', ' ': ' '}
  478. opposite_direction = ('B', 'D', 'DF', 'LDB', 'DB', 'L', 'LUB', 'LUF', 'LF', 'RU', 'LB', 'LDF', 'RD')
  479. """
  480.  
  481. direction_group = {}
  482. direction_group['X'] = 'LR'
  483. direction_group['Y'] = 'UD'
  484. direction_group['Z'] = 'FB'
  485. direction_group[' '] = ' '
  486.  
  487. #Come up with all possible directions
  488. all_directions = set()
  489. for x in [' ', 'X']:
  490. for y in [' ', 'Y']:
  491. for z in [' ', 'Z']:
  492. x_directions = list(direction_group[x])
  493. y_directions = list(direction_group[y])
  494. z_directions = list(direction_group[z])
  495. for i in x_directions:
  496. for j in y_directions:
  497. for k in z_directions:
  498. all_directions.add((i+j+k).replace(' ', ''))
  499.  
  500. #Narrow list down to remove any opposite directions
  501. opposite_direction = all_directions.copy()
  502. for i in all_directions:
  503. if i in opposite_direction:
  504. new_direction = ''
  505. for j in list(i):
  506. for k in direction_group.values():
  507. if j in k:
  508. new_direction += k.replace(j, '')
  509. opposite_direction.remove(new_direction)
  510.  
  511.  
  512. class PointConversion(object):
  513. """Used to convert the cell ID to 3D coordinates or vice versa.
  514. Mainly used for inputting the coordinates to make a move.
  515.  
  516. The cell ID is from 0 to grid_size^3, and coordinates are from 1 to grid_size.
  517. This means an ID of 0 is actually (1,1,1), and 3 would be (4,1,1).
  518.  
  519. - X -
  520. __1___2_
  521. / 1/ 0 / 1 /|
  522. Y /___/___/ |
  523. / 2/ 2 / 3 / |
  524. /___/___/ |
  525. | |____|___|
  526. | 1| / 4 /|5 /
  527. Z | /___/_|_/
  528. | 2| / 6 / 7|/
  529. |/___/___|
  530.  
  531. Parameters:
  532. grid_size:
  533. Size of the grid.
  534. Type: int
  535.  
  536. i:
  537. Cell ID or coordinates.
  538. Type int/tuple/list
  539.  
  540. Functions:
  541. to_3d
  542. to_int
  543. """
  544. def __init__(self, grid_size, i):
  545. self.grid_size = grid_size
  546. self.i = i
  547.  
  548. def to_3d(self):
  549. """Convert cell ID to a 3D coordinate.
  550.  
  551. >>> grid_size = 4
  552. >>> cell_id = 16
  553.  
  554. >>> PointConversion(grid_size, cell_id).to_3d()
  555. (1, 1, 2)
  556. """
  557. cell_id = int(self.i)
  558. z = cell_id / pow(self.grid_size, 2)
  559. cell_id %= pow(self.grid_size, 2)
  560. y = cell_id / self.grid_size
  561. x = cell_id % self.grid_size
  562. return tuple(cell_id+1 for cell_id in (x, y, z))
  563.  
  564. def to_int(self):
  565. """Convert 3D coordinates to the cell ID.
  566.  
  567. >>> grid_size = 4
  568. >>> coordinates = (4,2,3)
  569.  
  570. >>> PointConversion(grid_size, coordinates).to_int()
  571. 39
  572. """
  573. x, y, z = [int(i) for i in self.i]
  574. if all(i > 0 for i in (x, y, z)):
  575. return (x-1)*pow(self.grid_size, 0) + (y-1)*pow(self.grid_size, 1) + (z-1)*pow(self.grid_size, 2)
  576. return None
  577.  
  578.  
  579. class SwapGridData(object):
  580. """Use the size of the grid to calculate how flip it on the X, Y, or Z axis.
  581. The flips keep the grid intact but change the perspective of the game.
  582.  
  583. Parameters:
  584. grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.
  585. """
  586. def __init__(self, grid_data):
  587. self.grid_data = list(grid_data)
  588. self.grid_size = calculate_grid_size(self.grid_data)
  589.  
  590. def x(self):
  591. """Flip on the X axis.
  592.  
  593. >>> SwapGridData(range(8)).x()
  594. [1, 0, 3, 2, 5, 4, 7, 6]
  595. >>> print Connect3D(_raw_data=''.join(str(i) if i != '' else ' ' for i in SwapGridData(range(8)).x()))
  596. ________
  597. / 1 / 0 /|
  598. /___/___/ |
  599. / 3 / 2 / |
  600. /___/___/ |
  601. | |____|___|
  602. | / 5 /|4 /
  603. | /___/_|_/
  604. | / 7 / 6|/
  605. |/___/___|
  606. """
  607. return join_list(x[::-1] for x in split_list(self.grid_data, self.grid_size))
  608.  
  609. def y(self):
  610. """Flip on the Y axis.
  611.  
  612. >>> SwapGridData(range(8)).y()
  613. [2, 3, 0, 1, 6, 7, 4, 5]
  614. >>> print Connect3D(_raw_data=''.join(str(i) if i != '' else ' ' for i in SwapGridData(range(8)).y()))
  615. ________
  616. / 2 / 3 /|
  617. /___/___/ |
  618. / 0 / 1 / |
  619. /___/___/ |
  620. | |____|___|
  621. | / 6 /|7 /
  622. | /___/_|_/
  623. | / 4 / 5|/
  624. |/___/___|
  625. """
  626. group_split = split_list(self.grid_data, pow(self.grid_size, 2))
  627. return join_list(join_list(split_list(x, self.grid_size)[::-1]) for x in group_split)
  628.  
  629. def z(self):
  630. """Flip on the Z axis.
  631.  
  632. >>> SwapGridData(range(8)).z()
  633. [4, 5, 6, 7, 0, 1, 2, 3]
  634. >>> print Connect3D(_raw_data=''.join(str(i) if i != '' else ' ' for i in SwapGridData(range(8)).z()))
  635. ________
  636. / 4 / 5 /|
  637. /___/___/ |
  638. / 6 / 7 / |
  639. /___/___/ |
  640. | |____|___|
  641. | / 0 /|1 /
  642. | /___/_|_/
  643. | / 2 / 3|/
  644. |/___/___|
  645. """
  646. return join_list(split_list(self.grid_data, pow(self.grid_size, 2))[::-1])
  647.  
  648. def reverse(self):
  649. """Reverse the grid.
  650.  
  651. >>> SwapGridData(range(8)).reverse()
  652. [7, 6, 5, 4, 3, 2, 1, 0]
  653. >>> print Connect3D(_raw_data=''.join(str(i) if i != '' else ' ' for i in SwapGridData(range(8)).reverse()))
  654. ________
  655. / 7 / 6 /|
  656. /___/___/ |
  657. / 5 / 4 / |
  658. /___/___/ |
  659. | |____|___|
  660. | / 3 /|2 /
  661. | /___/_|_/
  662. | / 1 / 0|/
  663. |/___/___|
  664. """
  665. return self.grid_data[::-1]
  666.  
  667.  
  668. def calculate_grid_size(grid_data):
  669. """Cube root the length of grid_data to find the grid size."""
  670. return int(round(pow(len(grid_data), 1.0/3.0), 0))
  671.  
  672.  
  673. def split_list(x, n):
  674. """Split a list by n characters."""
  675. n = int(n)
  676. return [x[i:i+n] for i in range(0, len(x), n)]
  677.  
  678.  
  679. def join_list(x):
  680. """Convert nested lists into one single list."""
  681. return [j for i in x for j in i]
  682.  
  683.  
  684. def get_max_dict_keys(x):
  685. """Return a list of every key containing the max value.
  686.  
  687. Parameters:
  688. x (dict): Dictionary to sort and get highest value.
  689. It must be a dictionary of integers to work properly.
  690. """
  691. if x:
  692. sorted_dict = sorted(x.iteritems(), key=operator.itemgetter(1), reverse=True)
  693. if sorted_dict[0][1]:
  694. return sorted([k for k, v in x.iteritems() if v == sorted_dict[0][1]])
  695. return []
  696.  
  697.  
  698. class SimpleC3DAI(object):
  699. """AI coded to play Connect3D."""
  700.  
  701. def __init__(self, C3DObject, player_num):
  702. """Set up the AI for a single move using the current state of Connect3D."""
  703. self.C3DObject = C3DObject
  704. self.player_num = player_num
  705. self.player = self.C3DObject.player_symbols[self.player_num]
  706. self.enemy = self.C3DObject.player_symbols[int(not self.player_num)]
  707. self.gd_len = len(self.C3DObject.grid_data)
  708.  
  709. def max_cell_points(self):
  710. """Get maximum number of points that can be gained from each empty cell,
  711. that is not blocked by an enemy value.
  712. """
  713. max_points = defaultdict(int)
  714. filled_grid_data = [i if i else self.player for i in self.C3DObject.grid_data]
  715. for cell_id in range(self.gd_len):
  716. max_points[cell_id] += self.check_grid(filled_grid_data, cell_id, self.player)
  717. return get_max_dict_keys(max_points)
  718.  
  719. def check_for_three(self, grid_data=None):
  720. """Find all places where anyone has 3 points in a row.
  721. By looking at all the empty spaces, by substituting in
  722.  
  723. Parameters:
  724. grid_data (optional):
  725. Pass in a custom grid_data, leave as None to use the Connect3D one.
  726. Default: None
  727. """
  728. if grid_data is None:
  729. grid_data = list(self.C3DObject.grid_data)
  730.  
  731. matches = defaultdict(list)
  732. for cell_id in range(len(grid_data)):
  733. if not grid_data[cell_id]:
  734. for current_player in (self.player, self.enemy):
  735. if self.check_grid(grid_data, cell_id, current_player):
  736. matches[current_player].append(cell_id)
  737. return matches
  738.  
  739. def look_ahead(self):
  740. """Look two moves ahead to detect if someone could get a point.
  741. Uses the check_for_three function from within a loop.
  742.  
  743. Will return 1 as the second parameter if it has looked up more than a single move.
  744. """
  745. #Try initial check
  746. match = self.check_for_three()
  747. if match:
  748. return (match, 0)
  749.  
  750. #For every grid cell, substitute a player into it, then do the check again
  751. grid_data = list(self.C3DObject.grid_data)
  752. for i in range(self.gd_len):
  753. if not self.C3DObject.grid_data[i]:
  754. old_value = grid_data[i]
  755. for current_player in (self.player, self.enemy):
  756. grid_data[i] = current_player
  757. match = self.check_for_three(grid_data)
  758. if match:
  759. return (match, 1)
  760. grid_data[i] = old_value
  761.  
  762. return (defaultdict(list), 0)
  763.  
  764. def check_grid(self, grid_data, cell_id, player):
  765. """Duplicate of the Connect3D.update_score method, but set up to check individual cells.
  766.  
  767. Parameters:
  768. grid_data (list/tuple): 1D list of grid cells, amount must be a cube number.
  769.  
  770. cell_id (int): The cell ID, or grid_data index to update.
  771.  
  772. player (int): Integer representation of the player, can be 0 or 1.
  773. """
  774. max_points = 0
  775. for i in DirectionCalculation().opposite_direction:
  776.  
  777. #Get a list of directions and calculate movement amount
  778. possible_directions = [list(i)]
  779. possible_directions += [[j.replace(i, '') for i in possible_directions[0] for j in DirectionCalculation().direction_group.values() if i in j]]
  780. direction_movement = sum(self.C3DObject.direction_maths[j] for j in possible_directions[0])
  781.  
  782. #Build list of invalid directions
  783. invalid_directions = [[self.C3DObject.direction_edges[j] for j in possible_directions[k]] for k in (0, 1)]
  784. invalid_directions = [join_list(j) for j in invalid_directions]
  785.  
  786. num_matches = 1
  787.  
  788. #Use two loops for the opposite directions
  789. for j in (0, 1):
  790.  
  791. current_point = cell_id
  792.  
  793. while current_point not in invalid_directions[j] and 0 < current_point < len(grid_data):
  794. current_point += direction_movement * int('-'[:j] + '1')
  795. if grid_data[current_point] == player:
  796. num_matches += 1
  797. else:
  798. break
  799.  
  800. #Add a point if enough matches
  801. if num_matches == self.C3DObject.grid_size:
  802. max_points += 1
  803.  
  804. return max_points
  805.  
  806. def calculate_next_move(self):
  807. """Groups together the AI methods in order of importance.
  808. Will throw an error if grid_data is full, since the game should have ended by then anyway.
  809. """
  810.  
  811. point_based_move, far_away = SimpleC3DAI(self.C3DObject, self.player_num).look_ahead()
  812. order_of_importance = [self.enemy, self.player][::int('-'[:int(far_away)]+'1')]
  813.  
  814. if point_based_move[self.enemy]:
  815. next_moves = point_based_move[self.enemy]
  816. print 'AI State: Blocking opposing player.'
  817.  
  818. elif point_based_move[self.player]:
  819. next_moves = point_based_move[self.player]
  820. print 'AI State: Gaining points.'
  821.  
  822. else:
  823. next_moves = self.max_cell_points()
  824. print 'AI State: Random placement.'
  825.  
  826. if not next_moves:
  827. next_moves = [i for i in range(self.gd_len) if not self.C3DObject.grid_data[i]]
  828. print 'AI State: Struggling.'
  829.  
  830. return random.choice(next_moves)
  831.  
  832. if __name__ == '__main__':
  833. c3d = Connect3D()
  834. c3d.play()
  835.  
  836. from collections import defaultdict
  837. import itertools
  838. import operator
  839. import random
  840.  
  841. def __init__(self, grid_size=4, _raw_data=None):
  842. """Set up the grid and which player goes first.
  843.  
  844. Parameters:
  845. grid_size (int, optional): How long each side of the grid should
  846. be. The game works best with even numbers, 4 is the default and
  847. recommended.
  848.  
  849. _raw_data (str or None, optional): Passed in from ``__repr__``,
  850. contains the grid data and current player.
  851. Format: ``'joined(grid_data).current_player'``
  852.  
  853. ...
  854.  
  855. """
  856.  
  857. if __name__ == '__main__':
  858. import doctest
  859. doctest.testmod()
  860.  
  861. player_one, player_two = player_symbols # ValueError if not two characters
  862. if player_one == player_two:
  863. raise ValueError('symbols must be different')
  864.  
  865. class Connect3D(object):
  866.  
  867. def __init__(self, grid_size):
  868. self.grid_data = ['' for i in range(pow(grid_size, 3))]
  869. self.current_player = 0
  870. ... # carry out rest of grid_size-based setup
  871.  
  872. @classmethod
  873. def from_string(cls, raw_data):
  874. grid_data, current_player = raw_data.split('.')
  875. grid_data = [i if i != ' ' else '' for i in grid_data]
  876. grid = cls(calculate_grid_size(grid_data)) # create new instance
  877. ... # populate the instance with grid_data and current_player
  878. return grid # return new instance
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement