import random import time import numpy as np from ..Qt import QtWidgets, QtCore, QtGui __all__ = ['Game', 'QMines'] class Game(QtWidgets.QDialog): def __init__(self, mode, parent=None): super().__init__(parent=parent) layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(3, 3, 3, 3) self.label = QtWidgets.QLabel(self) layout.addWidget(self.label) self.startbutton = QtWidgets.QPushButton('Start', self) self.startbutton.clicked.connect(self.start) layout.addWidget(self.startbutton) if mode == 'tetris': self._setup_tetris() else: self._setup_snake() layout.addWidget(self.board) self.setStyleSheet(""" Board { border: 5px solid black; } QPushButton { font-weight: bold; } """) self.setLayout(layout) self.board.new_status.connect(self.new_message) def _setup_tetris(self): self.board = TetrisBoard(self) self.setGeometry(200, 100, 276, 546+self.startbutton.height()+self.label.height()) self.setWindowTitle('Totally not Tetris') def _setup_snake(self): self.board = SnakeBoard(self) self.setGeometry(200, 100, 406, 406+self.startbutton.height()+self.label.height()) self.setWindowTitle('Snakey') def start(self): self.board.start() @QtCore.pyqtSlot(str) def new_message(self, msg: str): self.label.setText(msg) class Board(QtWidgets.QFrame): new_status = QtCore.pyqtSignal(str) SPEED = 1000 WIDTH = 10 HEIGHT = 10 def __init__(self, parent=None): super().__init__(parent=parent) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.next_move) self.score = 0 self._speed = self.SPEED self._ispaused = False self._isdead = True self.setFrameStyle(QtWidgets.QFrame.Box | QtWidgets.QFrame.Raised) self.setLineWidth(3) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) def _init_game(self): raise NotImplementedError @property def cellwidth(self): return int(self.contentsRect().width() // self.WIDTH) # square height @property def cellheight(self): return int(self.contentsRect().height() // self.HEIGHT) def start(self): if self._isdead: self._init_game() self.new_status.emit(f'Score: {self.score}') self.timer.start(self._speed) self._isdead = False self.setFocus() def stop(self, msg): self.new_status.emit(f'Score {self.score} // ' + msg) self._isdead = True self.timer.stop() def pause(self): if self._ispaused and not self._isdead: self.new_status.emit(f'Score {self.score}') self.timer.start(self._speed) else: self.new_status.emit(f'Score {self.score} // Paused') self.timer.stop() self._ispaused = not self._ispaused def next_move(self): raise NotImplementedError def draw_square(self, painter, x, y, color): color = QtGui.QColor(color) painter.fillRect(x+1, y+1, self.cellwidth-2, self.cellheight-2, color) def draw_circle(self, painter, x, y, color): painter.save() color = QtGui.QColor(color) painter.setPen(QtGui.QPen(color)) painter.setBrush(QtGui.QBrush(color)) painter.drawEllipse(x+1, y+1, self.cellwidth, self.cellheight) painter.restore() def keyPressEvent(self, evt): if evt.key() == QtCore.Qt.Key_P: self.pause() else: super().keyPressEvent(evt) class SnakeBoard(Board): SPEED = 125 WIDTH = 35 HEIGHT = 35 def __init__(self, parent=None): super().__init__(parent=parent) self.snake = [[int(SnakeBoard.WIDTH//2), int(SnakeBoard.HEIGHT//2)], [int(SnakeBoard.WIDTH//2)+1, int(SnakeBoard.HEIGHT//2)]] self.current_x_head, self.current_y_head = self.snake[0] self.direction = 'l' self.next_direction = [] self._direction_pairs = {'l': 'r', 'u': 'd', 'r': 'l', 'd': 'u'} self.food = None self.grow_snake = False def _init_game(self): self.snake = [[int(SnakeBoard.WIDTH//2), int(SnakeBoard.HEIGHT//2)], [int(SnakeBoard.WIDTH//2)+1, int(SnakeBoard.HEIGHT//2)]] self.current_x_head, self.current_y_head = self.snake[0] self.direction = 'l' self.food = None self.grow_snake = False self.new_food() def next_move(self): self.snake_move() self.got_food() self.check_death() self.update() def snake_move(self): if self.next_direction: turn = self.next_direction.pop() if self.direction != self._direction_pairs[turn]: self.direction = turn if self.direction == 'l': self.current_x_head -= 1 elif self.direction == 'r': self.current_x_head += 1 # y increases top to bottom elif self.direction == 'u': self.current_y_head -= 1 elif self.direction == 'd': self.current_y_head += 1 head = (self.current_x_head, self.current_y_head) self.snake.insert(0, head) if not self.grow_snake: self.snake.pop() else: self.new_status.emit(f'Score: {self.score}') self.grow_snake = False def got_food(self): head = self.snake[0] if self.food == head: self.new_food() self.grow_snake = True self.score += 1 def new_food(self): x = random.randint(3, SnakeBoard.WIDTH-3) y = random.randint(3, SnakeBoard.HEIGHT-3) while (x, y) in self.snake: x = random.randint(3, SnakeBoard.WIDTH-3) y = random.randint(3, SnakeBoard.HEIGHT-3) self.food = (x, y) def check_death(self): rip_message = '' is_dead = False if (self.current_x_head < 1) or (self.current_x_head > SnakeBoard.WIDTH-2) or \ (self.current_y_head < 1) or (self.current_y_head > SnakeBoard.HEIGHT-2): rip_message = 'Snake found wall :(' is_dead = True head = self.snake[0] for snake_i in self.snake[1:]: if snake_i == head: rip_message = 'Snake bit itself :(' is_dead = True break if is_dead: self.stop(rip_message) def keyPressEvent(self, event): key = event.key() if key in (QtCore.Qt.Key_Left, QtCore.Qt.Key_A): self.next_direction.append('l') elif key in (QtCore.Qt.Key_Right, QtCore.Qt.Key_D): self.next_direction.append('r') elif key in (QtCore.Qt.Key_Down, QtCore.Qt.Key_S): self.next_direction.append('d') elif key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_W): self.next_direction.append('u') else: return super().keyPressEvent(event) def paintEvent(self, event): painter = QtGui.QPainter(self) rect = self.contentsRect() boardtop = rect.bottom() - SnakeBoard.HEIGHT * self.cellheight boardleft = rect.left() for pos in self.snake: self.draw_circle(painter, int(boardleft+pos[0]*self.cellwidth), int(boardtop+pos[1]*self.cellheight), 'blue') if self.food is not None: self.draw_square(painter, int(boardleft+self.food[0]*self.cellwidth), int(boardtop+self.food[1]*self.cellheight), 'orange') class TetrisBoard(Board): WIDTH = 10 HEIGHT = 20 SPEED = 300 def __init__(self, parent=None): super().__init__(parent=parent) self._shapes = { 1: ZShape, 2: SShape, 3: Line, 4: TShape, 5: Square, 6: MirrorL } self._init_game() def _init_game(self): self.curr_x = 0 self.curr_y = 0 self.curr_piece = None self.board = np.zeros((TetrisBoard.WIDTH, TetrisBoard.HEIGHT + 2), dtype=int) def next_move(self): if self.curr_piece is None: self.make_new_piece() else: self.move_down() def try_move(self, piece, x, y): if piece is None: return False if x+piece.x.min() < 0 or x+piece.x.max() >= TetrisBoard.WIDTH: return False if y-piece.y.max() < 0 or y-piece.y.min() >= TetrisBoard.HEIGHT+2: return False if np.any(self.board[piece.x+x, y-piece.y]) != 0: return False if piece != self.curr_piece: self.curr_piece = piece self.curr_x = x self.curr_y = y self.update() return True def make_new_piece(self): new_piece = self._shapes[random.randint(1, len(self._shapes))]() startx = TetrisBoard.WIDTH//2 starty = TetrisBoard.HEIGHT+2 - 1 + new_piece.y.min() if not self.try_move(new_piece, startx, starty): self.stop('Game over :(') def move_down(self): if not self.try_move(self.curr_piece, self.curr_x, self.curr_y-1): self.final_destination_reached() def drop_to_bottom(self): new_y = self.curr_y while new_y > 0: if not self.try_move(self.curr_piece, self.curr_x, new_y-1): break new_y -= 1 self.final_destination_reached() def final_destination_reached(self): x = self.curr_x+self.curr_piece.x y = self.curr_y-self.curr_piece.y self.board[x, y] = next(k for k, v in self._shapes.items() if isinstance(self.curr_piece, v)) self.remove_lines() self.curr_piece = None self.make_new_piece() def remove_lines(self): full_rows = np.where(np.all(self.board, axis=0))[0] num_rows = len(full_rows) if num_rows: temp = np.zeros_like(self.board) temp[:, :temp.shape[1]-num_rows] = np.delete(self.board, full_rows, axis=1) self.board = temp self.score += num_rows self.new_status.emit(f'Lines: {self.score}') if self.score % 10 == 0: self._speed += 0.9 self.timer.setInterval(int(self._speed)) self.update() def keyPressEvent(self, event): key = event.key() if self.curr_piece is None: return super().keyPressEvent(event) if key == QtCore.Qt.Key_Left: self.try_move(self.curr_piece, self.curr_x-1, self.curr_y) elif key == QtCore.Qt.Key_Right: self.try_move(self.curr_piece, self.curr_x+1, self.curr_y) elif key == QtCore.Qt.Key_Down: if not self.try_move(self.curr_piece.rotate(), self.curr_x, self.curr_y): self.curr_piece.rotate(clockwise=False) elif key == QtCore.Qt.Key_Up: if not self.try_move(self.curr_piece.rotate(clockwise=False), self.curr_x, self.curr_y): self.curr_piece.rotate() elif key == QtCore.Qt.Key_Space: self.drop_to_bottom() else: super().keyPressEvent(event) def paintEvent(self, event): painter = QtGui.QPainter(self) rect = self.contentsRect() board_top = rect.bottom() - TetrisBoard.HEIGHT*self.cellheight for i in range(TetrisBoard.WIDTH): for j in range(TetrisBoard.HEIGHT): shape = self.board[i, j] if shape: color = self._shapes[shape].color self.draw_square(painter, rect.left() + i*self.cellwidth, board_top + (TetrisBoard.HEIGHT-j-1)*self.cellheight, color) if self.curr_piece is not None: x = self.curr_x + self.curr_piece.x y = self.curr_y - self.curr_piece.y for i in range(4): if TetrisBoard.HEIGHT < y[i]+1: continue self.draw_square(painter, rect.left() + x[i] * self.cellwidth, board_top + (TetrisBoard.HEIGHT-y[i]-1) * self.cellheight, self.curr_piece.color) class Tetromino: SHAPE = np.array([[0], [0]]) color = None def __init__(self): self.shape = self.SHAPE def rotate(self, clockwise: bool = True): if clockwise: self.shape = np.vstack((-self.shape[1], self.shape[0])) else: self.shape = np.vstack((self.shape[1], -self.shape[0])) return self @property def x(self): return self.shape[0] @property def y(self): return self.shape[1] class ZShape(Tetromino): SHAPE = np.array([[0, 0, -1, -1], [-1, 0, 0, 1]]) color = 'green' class SShape(Tetromino): SHAPE = np.array([[0, 0, 1, 1], [-1, 0, 0, 1]]) color = 'purple' class Line(Tetromino): SHAPE = np.array([[0, 0, 0, 0], [-1, 0, 1, 2]]) color = 'red' class TShape(Tetromino): SHAPE = np.array([[-1, 0, 1, 0], [0, 0, 0, 1]]) color = 'orange' class Square(Tetromino): SHAPE = np.array([[0, 1, 0, 1], [0, 0, 1, 1]]) color = 'yellow' class LShape(Tetromino): SHAPE = np.array([[-1, 0, 0, 0], [1, -1, 0, 1]]) color = 'blue' class MirrorL(Tetromino): SHAPE = np.array([[1, 0, 0, 0], [-1, -1, 0, 1]]) color = 'lightGray' class Field(QtWidgets.QToolButton): NUM_COLORS = { 1: '#1e46a4', 2: '#f28e2b', 3: '#e15759', 4: '#76b7b2', 5: '#59a14f', 6: '#edc948', 7: '#b07aa1', 8: '#ff9da7', 'X': '#ff0000', } flag_change = QtCore.pyqtSignal(bool) def __init__(self, x, y, parent=None): super(Field, self).__init__(parent=parent) self.setFixedSize(QtCore.QSize(30, 30)) f = self.font() f.setPointSize(24) f.setWeight(75) self.setFont(f) self.x = x self.y = y self.neighbors = [] self.grid = self.parent() _size = self.grid.gridsize for x_i in range(max(0, self.x-1), min(self.x+2, _size[0])): for y_i in range(max(0, self.y-1), min(self.y+2, _size[1])): if (x_i, y_i) == (self.x, self.y): continue self.neighbors.append((x_i, y_i)) self.is_mine = False self.is_flagged = False self.is_known = False self.has_died = False def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if self.grid.status == 'finished': return if evt.button() == QtCore.Qt.RightButton: self.set_flag() elif evt.button() == QtCore.Qt.LeftButton: self.select() else: super().mousePressEvent(evt) def __str__(self) -> str: if self.is_mine: return 'X' else: return str(self.visible_mines) @property def visible_mines(self) -> int: return sum(self.grid.map[x_i][y_i].is_mine for x_i, y_i in self.neighbors) @QtCore.pyqtSlot() def select(self): if self.is_flagged: return if self.is_mine: self.setText('X') num = 'X' self.has_died = True self.setStyleSheet(f'color: {self.NUM_COLORS[num]}') else: self.is_known = True self.setEnabled(False) self.setAutoRaise(True) num = self.visible_mines if num != 0: self.setText(str(num)) self.setStyleSheet(f'color: {self.NUM_COLORS[num]}') else: self.grid.reveal_neighbors(self) self.clicked.emit() @QtCore.pyqtSlot() def set_flag(self): if self.is_flagged: self.setText('') else: self.setText('!') self.is_flagged = not self.is_flagged self.has_died = False self.clicked.emit() self.flag_change.emit(self.is_flagged) class QMines(QtWidgets.QMainWindow): LEVELS = { 'easy': ((9, 9), 10), 'middle': ((16, 16), 40), 'hard': ((16, 30), 99), 'very hard': ((16, 30), 10), } def __init__(self, parent=None): super().__init__(parent=parent) self.map = [] self.status = 'waiting' self.open_fields = 0 self.num_flags = 0 self._start = 0 self.gridsize, self.mines = (1, 1), 1 self._init_ui() def _init_ui(self): layout = QtWidgets.QGridLayout() layout.setSpacing(0) self.central = QtWidgets.QWidget() self.setCentralWidget(self.central) layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding), 0, 0) self.grid_layout = QtWidgets.QGridLayout() self.grid_layout.setSpacing(0) self.map_widget = QtWidgets.QFrame() self.map_widget.setFrameStyle(2) self.map_widget.setLayout(self.grid_layout) layout.addWidget(self.map_widget, 1, 1) self.new_game = QtWidgets.QPushButton('New game') self.new_game.pressed.connect(self._init_map) layout.addWidget(self.new_game, 0, 1) layout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding), 2, 2) self.central.setLayout(layout) self.timer = QtCore.QTimer() self.timer.setInterval(1000) self.timer.timeout.connect(self.update_time) self.timer_message = QtWidgets.QLabel('0 s') self.statusBar().addWidget(self.timer_message) self.mine_message = QtWidgets.QLabel(f'0 / {self.mines}') self.statusBar().addWidget(self.mine_message) self.dead_message = QtWidgets.QLabel('') self.statusBar().addWidget(self.dead_message) self.level_cb = QtWidgets.QComboBox() self.level_cb.addItems(list(self.LEVELS.keys())) self.level_cb.currentTextChanged.connect(self._init_map) self.statusBar().addPermanentWidget(self.level_cb) self._init_map('easy') def _init_map(self, lvl: str = None): if lvl is None: lvl = self.level_cb.currentText() self._clear_map() self.gridsize, self.mines = QMines.LEVELS[lvl] w, h = self.gridsize self.map = [[None] * h for _ in range(w)] for x in range(w): for y in range(h): pos = Field(x, y, parent=self) pos.clicked.connect(self.start_game) pos.clicked.connect(self.update_status) pos.flag_change.connect(self.update_flag) self.grid_layout.addWidget(pos, x+1, y+1) self.map[x][y] = pos self.set_mines() def _clear_map(self): self.status = 'waiting' self.open_fields = 0 self.num_flags = 0 self._start = 0 self.map = [] while self.grid_layout.count(): child = self.grid_layout.takeAt(0) w = child.widget() if w: self.grid_layout.removeWidget(w) w.deleteLater() else: self.grid_layout.removeItem(child) def set_mines(self): count = 0 w, h = self.gridsize while count < self.mines: n = random.randint(0, w * h - 1) row = n // h col = n % h pos = self.map[row][col] # type: Field if pos.is_mine: continue pos.is_mine = True count += 1 self.status = 'waiting' self.open_fields = 0 self.num_flags = 0 self._start = 0 self.timer_message.setText('0 s') self.mine_message.setText(f'0 / {self.mines}') self.dead_message.setText('') def reveal_neighbors(self, pos: Field): for x_i, y_i in pos.neighbors: field_i = self.map[x_i][y_i] # type: Field if field_i.isEnabled(): field_i.select() def start_game(self): if self.status == 'waiting': self.status = 'running' self._start = time.time() self.timer.start(1000) def update_time(self): if self.status == 'running': self.timer_message.setText(f'{time.time()-self._start:3.0f} s') def update_status(self): if self.status == 'finished': return pos = self.sender() # type: Field if pos.is_known: self.open_fields += 1 if self.open_fields == self.gridsize[0] * self.gridsize[1] - self.mines: self.timer.stop() _ = QtWidgets.QMessageBox.information(self, 'Game finished', 'Game finished!!!') self.status = 'finished' elif pos.has_died: dead_cnt = self.dead_message.text() if dead_cnt == '': self.dead_message.setText('(Deaths: 1)') else: self.dead_message.setText(f'(Deaths: {int(dead_cnt[9:-1])+1})') @QtCore.pyqtSlot(bool) def update_flag(self, state: bool): num_mines = int(self.mine_message.text().split()[0]) if state: num_mines += 1 else: num_mines -= 1 self.mine_message.setText(f'{num_mines} / {self.mines}')