Advertisement
Guest User

Untitled

a guest
May 16th, 2019
95
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 15.95 KB | None | 0 0
  1. # BOARD
  2.  
  3. from collections import deque
  4. import random
  5. import numpy
  6. import pygame
  7.  
  8.  
  9. LEFT_BUTTON = 1
  10. RIGHT_BUTTON = 3
  11.  
  12.  
  13. def cross_image(image, color, line_width):
  14.     """Draw a cross over the tile."""
  15.     image = image.copy()
  16.     w, h = image.get_size()
  17.     pygame.draw.line(image, color, (0, 0), (w, h), line_width)
  18.     pygame.draw.line(image, color, (w, 0), (0, h), line_width)
  19.     return image
  20.  
  21.  
  22. def add_background_color(tile, color):
  23.     """Draw a tile on the solid color background."""
  24.     background = pygame.Surface(tile.get_size())
  25.     background.fill(color)
  26.     background.blit(tile, (0, 0))
  27.     return background
  28.  
  29.  
  30. class Tile(pygame.sprite.Sprite):
  31.     """Sprite for a board tile."""
  32.     def __init__(self, image, i, j, tile_size):
  33.         super(Tile, self).__init__()
  34.         self.image = image
  35.         self.rect = pygame.Rect(j * tile_size, i * tile_size,
  36.                                 tile_size, tile_size)
  37.  
  38.  
  39. def create_field(n_rows, n_cols, tile_size, bg_color, line_color):
  40.     """Create a checkered field.
  41.  
  42.    Parameters
  43.    ----------
  44.    n_rows, n_cols : int
  45.        Number of rows and columns.
  46.    tile_size : int
  47.        Length of tile's side.
  48.    bg_color : pygame.Color compatible
  49.        Background color.
  50.    line_color : pygame.Color compatible
  51.        Color of lines.
  52.  
  53.    Returns
  54.    -------
  55.    pygame.Surface
  56.        Image of the field.
  57.    """
  58.     field = pygame.Surface((n_cols * tile_size, n_rows * tile_size))
  59.     field.fill(bg_color)
  60.  
  61.     for i in range(n_rows):
  62.         pygame.draw.line(field, line_color,
  63.                          (0, i * tile_size),
  64.                          (n_cols * tile_size, i * tile_size))
  65.  
  66.     for j in range(n_cols):
  67.         pygame.draw.line(field, line_color,
  68.                          (j * tile_size, 0),
  69.                          (j * tile_size, n_rows * tile_size))
  70.  
  71.     return field
  72.  
  73.  
  74. class Board:
  75.     """Game board.
  76.  
  77.    Parameters
  78.    ----------
  79.    n_rows : int
  80.        Number of rows.
  81.    n_cols : int
  82.        Number of columns.
  83.    n_mines : int
  84.        Number of columns. Must be not greater than ``n_rows * n_cols``.
  85.    tile_size : int
  86.        Length of a tile's side in pixels.
  87.    tile_image : pygame.Surface
  88.        Image for a closed tile.
  89.    mine_count_images : list of pygame.Surface
  90.        Nine images for mine counts (from 0 to 8).
  91.    flag_image : pygame.Surface
  92.        Image for a flag.
  93.    mine_image : pygame.Surface
  94.        Image for a mine.
  95.    on_status_change_callback : callable
  96.        Call when game status changes. The signature is
  97.        ``on_status_change_callback(news_status)``, where ``new_status`` is
  98.        one of ["before_start", "running", "victory", "game_over"].
  99.    """
  100.     TILE_CLOSED = 0
  101.     TILE_OPENED = 1
  102.     TILE_CHECKED = 2
  103.  
  104.     def __init__(self, n_rows, n_cols, n_mines, bg_color, bg_lines_color,
  105.                  tile_size, tile_image, mine_count_images, flag_image,
  106.                  mine_image, on_status_change_callback=None):
  107.         self.n_rows = n_rows
  108.         self.n_cols = n_cols
  109.         self.n_mines = n_mines
  110.         self.n_mines_left = n_mines
  111.  
  112.         self.is_mine = numpy.zeros((n_rows, n_cols), dtype=bool)
  113.         self.mine_count = None
  114.         self.tile_status = numpy.full((n_rows, n_cols), self.TILE_CLOSED,
  115.                                       dtype=numpy.int32)
  116.         self.losing_indices = None
  117.         self.start_time = None
  118.         self.tiles_to_open = self.n_rows * self.n_cols - self.n_mines
  119.         self._time = 0
  120.  
  121.         self.tile_size = tile_size
  122.  
  123.         self.bg_color = bg_color
  124.         self.bg_lines_color = bg_lines_color
  125.         self.bg_image = create_field(self.n_rows, self.n_cols,
  126.                                      self.tile_size, bg_color,
  127.                                      bg_lines_color)
  128.         self.tile_image = tile_image
  129.         self.mine_count_images = mine_count_images
  130.         self.flag_image = flag_image
  131.         self.mine_image = mine_image
  132.         self.mine_image_crossed = cross_image(mine_image,
  133.                                               pygame.Color('red'),
  134.                                               2)
  135.         self.mine_image_red_bg = add_background_color(mine_image,
  136.                                                       pygame.Color('red'))
  137.         self.rect = pygame.Rect(
  138.             0, 0, self.n_cols * self.tile_size, self.n_rows * self.tile_size)
  139.  
  140.         self.tiles = None
  141.         self.tiles_group = None
  142.         self._init_tiles()
  143.  
  144.         self.on_status_change_callback = on_status_change_callback
  145.         self.game_status = "before_start"
  146.  
  147.     def _init_tiles(self):
  148.         """Initialize list of tiles with closed tiles."""
  149.         self.tiles = []
  150.         for i in range(self.n_rows):
  151.             for j in range(self.n_cols):
  152.                 self.tiles.append(Tile(self.tile_image, i, j, self.tile_size))
  153.         self.tiles_group = pygame.sprite.Group(*self.tiles)
  154.  
  155.     def reset(self, n_rows=None, n_cols=None, n_mines=None):
  156.         """Reset board.
  157.  
  158.        Optional arguments will replace ones set in the class (if presented).
  159.        """
  160.         if n_mines is not None:
  161.             self.n_mines = n_mines
  162.  
  163.         if n_rows is not None or n_cols is not None:
  164.             if n_rows is None:
  165.                 n_rows = self.n_rows
  166.             if n_cols is None:
  167.                 n_cols = self.n_cols
  168.  
  169.             self.tile_status = numpy.empty((n_rows, n_cols), dtype=numpy.int32)
  170.             self.is_mine = numpy.zeros((n_rows, n_cols), dtype=bool)
  171.  
  172.             self.n_rows = n_rows
  173.             self.n_cols = n_cols
  174.  
  175.         self.bg_image = create_field(self.n_rows, self.n_cols, self.tile_size,
  176.                                      self.bg_color, self.bg_lines_color)
  177.         self.n_mines_left = self.n_mines
  178.         self.is_mine.fill(0)
  179.         self.mine_count = None
  180.         self.tile_status.fill(self.TILE_CLOSED)
  181.         self.losing_indices = None
  182.         self.start_time = None
  183.         self.tiles_to_open = self.n_rows * self.n_cols - self.n_mines
  184.         self._init_tiles()
  185.         self._time = 0
  186.         self._change_game_status("before_start")
  187.  
  188.     def _change_game_status(self, new_status):
  189.         self.game_status = new_status
  190.         if self.on_status_change_callback is not None:
  191.             self.on_status_change_callback(self.game_status)
  192.  
  193.     @property
  194.     def time(self):
  195.         """Return time passed from the game start."""
  196.         if self.game_status == 'running' and self.start_time is not None:
  197.             self._time = (pygame.time.get_ticks() - self.start_time) // 1000
  198.  
  199.         return self._time
  200.  
  201.     def _put_mines(self, i_click, j_click):
  202.         """Put mines after the first click."""
  203.         allowed_positions = []
  204.         n_tiles = self.n_rows * self.n_cols
  205.         for i in range(self.n_rows):
  206.             for j in range(self.n_cols):
  207.                 if any((
  208.                     abs(i - i_click) > 1 or abs(j - j_click) > 1,
  209.                     self.n_mines > n_tiles - 9
  210.                     and (i != i_click or j != j_click),
  211.                     self.n_mines == self.n_rows * self.n_cols
  212.                 )):
  213.                     allowed_positions.append((i, j))
  214.  
  215.         selected_positions = numpy.asarray(random.sample(allowed_positions,
  216.                                                          self.n_mines))
  217.         self.is_mine[selected_positions[:, 0], selected_positions[:, 1]] = True
  218.  
  219.         self.mine_count = numpy.zeros((self.n_rows, self.n_cols),
  220.                                       dtype=numpy.int8)
  221.  
  222.         for i, j in selected_positions:
  223.             ind = self._get_neighbors_flat(i, j)
  224.             self.mine_count.flat[ind] += 1
  225.  
  226.     def get_neighbors(self, i, j):
  227.         """Return pairs of indices of neighbor cells."""
  228.         ret = []
  229.  
  230.         if i > 0:
  231.             ret.append((i - 1, j))
  232.             if j > 0:
  233.                 ret.append((i - 1, j - 1))
  234.             if j < self.n_cols - 1:
  235.                 ret.append((i - 1, j + 1))
  236.  
  237.         if j > 0:
  238.             ret.append((i, j - 1))
  239.  
  240.         if j < self.n_cols - 1:
  241.             ret.append((i, j + 1))
  242.  
  243.         if i < self.n_rows - 1:
  244.             ret.append((i + 1, j))
  245.             if j > 0:
  246.                 ret.append((i + 1, j - 1))
  247.             if j < self.n_cols - 1:
  248.                 ret.append((i + 1, j + 1))
  249.  
  250.         return ret
  251.  
  252.     def _get_neighbors_flat(self, i, j):
  253.         """Return indices of neighbor cells as a single number."""
  254.         return [k * self.n_cols + l for (k, l) in self.get_neighbors(i, j)]
  255.  
  256.     def _open_tiles(self, i, j):
  257.         """Open tiles on click using the wave algorithm."""
  258.         queue = deque()
  259.         queue.append((i, j))
  260.         self.tile_status[i, j] = self.TILE_OPENED
  261.         self.tiles_to_open -= 1
  262.  
  263.         while queue:
  264.             i, j = queue.popleft()
  265.             if self.mine_count[i, j] > 0:
  266.                 continue
  267.  
  268.             neighbors = self.get_neighbors(i, j)
  269.             for k, l in neighbors:
  270.                 if self.tile_status[k, l] == self.TILE_CLOSED:
  271.                     self.tile_status[k, l] = self.TILE_OPENED
  272.                     self.tiles_to_open -= 1
  273.                     queue.append((k, l))
  274.  
  275.         if self.tiles_to_open == 0:
  276.             self._change_game_status("victory")
  277.             self.n_mines_left = 0
  278.             self.tile_status[self.is_mine] = self.TILE_CHECKED
  279.  
  280.     def _check_tile(self, i, j):
  281.         """Check tile with a flag (right click action)."""
  282.         if self.tile_status[i, j] == self.TILE_CLOSED:
  283.             self.tile_status[i, j] = self.TILE_CHECKED
  284.             self.n_mines_left -= 1
  285.         elif self.tile_status[i, j] == self.TILE_CHECKED:
  286.             self.tile_status[i, j] = self.TILE_CLOSED
  287.             self.n_mines_left += 1
  288.  
  289.     def _open_tile(self, i, j):
  290.         """Open tile (left click action)."""
  291.         status = self.tile_status[i, j]
  292.         if status == self.TILE_CHECKED:
  293.             return
  294.  
  295.         if self.is_mine[i, j]:
  296.             self._change_game_status("game_over")
  297.             self.losing_indices = (i, j)
  298.             self.tile_status[i, j] = self.TILE_OPENED
  299.             return
  300.  
  301.         if status == self.TILE_CLOSED:
  302.             self._open_tiles(i, j)
  303.             return
  304.  
  305.         if self.mine_count[i, j] == 0:
  306.             return
  307.  
  308.         neighbors = self.get_neighbors(i, j)
  309.         checked_count = sum(self.tile_status[k, l] == self.TILE_CHECKED
  310.                             for k, l in neighbors)
  311.         if checked_count == self.mine_count[i, j]:
  312.             for k, l in neighbors:
  313.                 if self.tile_status[k, l] == self.TILE_CLOSED:
  314.                     if self.is_mine[k, l]:
  315.                         self.losing_indices = (k, l)
  316.                         self._change_game_status("game_over")
  317.                         self.tile_status[k, l] = self.TILE_OPENED
  318.                         return
  319.  
  320.                     self._open_tiles(k, l)
  321.  
  322.     def _update_view_game_over(self):
  323.         """Update view for game over state."""
  324.         k = 0
  325.         for i in range(self.n_rows):
  326.             for j in range(self.n_cols):
  327.                 status = self.tile_status[i, j]
  328.                 tile = self.tiles[k]
  329.                 if self.is_mine[i, j]:
  330.                     if (i, j) == self.losing_indices:
  331.                         tile.image = self.mine_image_red_bg
  332.                     elif self.tile_status[i, j] == Board.TILE_CHECKED:
  333.                         tile.image = self.tile_image.copy()
  334.                         rect = self.flag_image.get_rect(
  335.                             center=tile.image.get_rect().center)
  336.                         tile.image.blit(self.flag_image, rect.topleft)
  337.                     else:
  338.                         tile.image = self.mine_image
  339.                 elif status == Board.TILE_CLOSED:
  340.                     tile.image = self.tile_image
  341.                 elif status == Board.TILE_OPENED:
  342.                     tile.image = self.mine_count_images[self.mine_count[i, j]]
  343.                 elif status == Board.TILE_CHECKED:
  344.                     tile.image = self.mine_image_crossed
  345.  
  346.                 k += 1
  347.  
  348.     def _prepare_highlight(self, i_hold, j_hold):
  349.         """Compute tile indices to highlight as being pressed.
  350.  
  351.        This happens when the mouse button is held down.
  352.        """
  353.         if i_hold is None or j_hold is None:
  354.             return set()
  355.  
  356.         if self.tile_status[i_hold, j_hold] == Board.TILE_CLOSED:
  357.             return {(i_hold, j_hold)}
  358.  
  359.         if self.tile_status[i_hold, j_hold] == Board.TILE_CHECKED:
  360.             return set()
  361.  
  362.         if self.mine_count[i_hold, j_hold] == 0:
  363.             return set()
  364.  
  365.         return {
  366.             (i, j) for i, j in self.get_neighbors(i_hold, j_hold)
  367.             if self.tile_status[i, j] == Board.TILE_CLOSED
  368.         }
  369.  
  370.     def _update_view_running(self):
  371.         """Update view in running state."""
  372.         if pygame.mouse.get_pressed()[0]:
  373.             i_hold, j_hold = self._get_tile_indices_at_mouse()
  374.             highlight = self._prepare_highlight(i_hold, j_hold)
  375.         else:
  376.             highlight = None
  377.  
  378.         k = 0
  379.         for i in range(self.n_rows):
  380.             for j in range(self.n_cols):
  381.                 status = self.tile_status[i, j]
  382.                 tile = self.tiles[k]
  383.  
  384.                 if highlight is not None and (i, j) in highlight:
  385.                     tile.image = self.mine_count_images[0]
  386.                     k += 1
  387.                     continue
  388.  
  389.                 if status == Board.TILE_CLOSED:
  390.                     tile.image = self.tile_image
  391.                 elif status == Board.TILE_OPENED:
  392.                     if self.is_mine[i, j]:
  393.                         tile.image = self.mine_image
  394.                     else:
  395.                         mine_count = self.mine_count[i, j]
  396.                         tile.image = self.mine_count_images[mine_count]
  397.                 elif status == Board.TILE_CHECKED:
  398.                     tile.image = self.tile_image.copy()
  399.                     rect = self.flag_image.get_rect(
  400.                         center=tile.image.get_rect().center)
  401.                     tile.image.blit(self.flag_image, rect.topleft)
  402.  
  403.                 k += 1
  404.  
  405.     def _get_tile_indices_at_mouse(self):
  406.         """Return tile indices at the mouse cursor.
  407.  
  408.        If mouse cursor is outside of the board region, one or both of the
  409.        indices will be None.
  410.        """
  411.         xm, ym = pygame.mouse.get_pos()
  412.         xc, yc = self.rect.topleft
  413.  
  414.         i = (ym - yc) // self.tile_size
  415.         j = (xm - xc) // self.tile_size
  416.  
  417.         if i < 0 or i >= self.n_rows:
  418.             i = None
  419.  
  420.         if j < 0 or j >= self.n_cols:
  421.             j = None
  422.  
  423.         return i, j
  424.  
  425.     def _update_view(self):
  426.         """Update board view."""
  427.         if self.game_status == "game_over":
  428.             self._update_view_game_over()
  429.         else:
  430.             self._update_view_running()
  431.  
  432.     def on_mouse_down(self, button):
  433.         """Process mouse button down."""
  434.         if self.game_status in ["before_start", "victory", "game_over"]:
  435.             return
  436.  
  437.         if button == RIGHT_BUTTON:
  438.             i, j = self._get_tile_indices_at_mouse()
  439.             if i is not None and j is not None:
  440.                 self._check_tile(i, j)
  441.                 self._update_view()
  442.  
  443.     def on_mouse_up(self, button):
  444.         """Process mouse button up."""
  445.         if self.game_status in ["victory", "game_over"]:
  446.             return
  447.  
  448.         if button == LEFT_BUTTON:
  449.             i, j = self._get_tile_indices_at_mouse()
  450.             if i is not None and j is not None:
  451.                 self._open_tile(i, j)
  452.                 self._update_view()
  453.  
  454.     def draw(self, surface):
  455.         """Draw board on surface."""
  456.         # In this case we need to update tiles being pressed due to the mouse hold.
  457.         if self.game_status in ["before_start", "running"]:
  458.             if pygame.mouse.get_pressed()[0]:
  459.                 self._update_view_running()
  460.  
  461.         bg = self.bg_image.copy()
  462.         self.tiles_group.draw(bg)
  463.         surface.blit(bg, self.rect)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement