示例#1
0
class GamePanel:

    ## Background color.
    BACKGROUND_COLOR = Color.GRAY
    ## Border color.
    BOUNDARY_COLOR = Color.GREY

    ##
    # Constructs a GamePanel with the given game associated ScorePanel.
    # @param game
    #   the IGame instance for which this is the UI.
    # @param windowID graphical context for this panel.
    # @param scorePanel
    #   panel for displaying scores associated with the game.
    #
    def __init__(self, game, windowID, scorePanel=None):
        ## Number of pixels each icon falls per frame when animating.
        self.fallPerFrame = 4
        ## Interval between frames when animating falling icons, in milliseconds.
        self.fallingSpeed = GameMain.SPEED
        ## Number of times to flash when a pair is selected.
        self.numberOfFlashes = 5
        ## Interval between flashes, in milliseconds.
        self.flashingSpeed = 150
        ## State variable counts down to zero while flashing the cells to be collapsed.
        self.flashingState = 0
        ## Cells about to be collapsed are flashed briefly before moving.
        self.cellsToCollapse = None
        ## Flag indicates whether we are currently moving cells down.
        self.collapsing = False
        ## Cells currently being moved down during a collapse.
        self.movingCells = None
        ## Flag indicates whether we are currently filling new cells.
        self.filling = False
        ## New cells filled from top.
        self.fillingCells = None
        ## Cell currently selected by a mouse down event.
        self.currentCell = None
        ## Cell currently selected by a mouse drag event.
        self.nextCell = None
        self.setStroke(1)
        ## Window running this panel.
        self.g = windowID
        glutSetWindow(self.g)

        ## Dictionary of colors.
        # @see https://www.2020colours.com
        self.colors = {
            Color.GRAY: [0.5, 0.5, 0.5],
            Color.GREY: [0.33, 0.33, 0.33],
            Color.RED: [1.0, 0.0, 0.0],
            Color.GREEN: [0.0, 1.0, 0.0],
            Color.YELLOW: [1.0, 1.0, 0.0],
            Color.BLUE: [0.0, 0.0, 1.0],
            Color.CYAN: [0.0, 1.0, 1.0],
            Color.MAGENTA: [1.0, 0.0, 1.0],
            Color.BURGUNDY: [0.5, 0.0, 0.13],
            Color.GARNET: [0.6, 0.02, 0.3],
            Color.PINK: [0.98, 0.68, 0.82],
            Color.AMETHYST: [0.6, 0.4, 0.8],
            Color.EMERALD: [0.31, 0.78, 0.47],
            Color.DIAMOND: [0.73, 0.95, 1.0],
            Color.TOPAZ: [1.0, 0.78, 0.49],
            Color.RUBY: [0.88, 0.07, 0.37],
            Color.SAPPHIRE: [0.06, 0.32, 0.73],
            Color.WHITE: [1.0, 1.0, 1.0],
            Color.BLACK: [0.0, 0.0, 0.0],
            Color.ORANGE: [1.0, 0.5, 0.0]
        }

        ## The IGame instance for which this is the UI.
        self.game = game
        ## Score panel associated with the game.
        self.scorePanel = scorePanel
        ## Background color.
        self.backgroundColor = self.colors[GamePanel.BACKGROUND_COLOR] + [1.0]
        ## Window height.
        self.h = self.game.getHeight() * GameMain.SIZE
        ## Window width.
        self.w = self.game.getWidth() * GameMain.SIZE
        ## using two viewports
        self.twoViewp = (self.g == self.scorePanel.g)

        glutMouseFunc(self.mousePressed)
        glutMotionFunc(self.mouseDragged)
        glutDisplayFunc(self.TimerCallback)
        glutReshapeFunc(self.reshape)
        glClearColor(*self.backgroundColor)

        ## Timer instance used to animate the UI.
        self.timer = Timer(self.fallingSpeed,
                           glutPostRedisplay)  # self.TimerCallback
        self.timer.run()

    ## Reshape callback.
    #
    # Since cell (0,0) is at the upper left corner of the grid,
    # we draw the Y axis upside down.
    #
    # @param w new window width.
    # @param h new window height.
    #
    def reshape(self, w, h):
        if self.twoViewp:
            w //= 2
        aspect = float(self.game.getHeight()) / float(self.game.getWidth())
        if w * aspect > h:
            GameMain.SIZE = h // self.game.getHeight()
        else:
            GameMain.SIZE = w // self.game.getWidth()
        self.h = self.game.getHeight() * GameMain.SIZE
        self.w = self.game.getWidth() * GameMain.SIZE
        self.d = abs(h - self.h)

        # (0,0) at upper left corner.
        xmin = w if self.twoViewp else 0
        glViewport(xmin, self.d, self.w, self.h)
        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        # Y axis upside down.
        glOrtho(0, self.w, self.h, 0, -1, 1)
        if GameMain.__DEBUG__:
            print('Resizing game window')

    ## Force a repaint of this panel.
    def repaint(self):
        glutSetWindow(self.g)
        self.paintComponent()

        # two viewports.
        if self.twoViewp:
            w = glutGet(GLUT_WINDOW_WIDTH) // 2
            h = glutGet(GLUT_WINDOW_HEIGHT)
            # set the viewport for the panel.
            self.scorePanel.reshape(w, h)
            self.setColor(Color.WHITE)
            # clean only the score panel, drawing a rectangle
            if h > w:
                self.fillRect(0, 0, h, w)
            else:
                self.fillRect(0, 0, w, h)
            self.scorePanel.paintComponent(False)
            glutSwapBuffers()
            # restore the viewport for the game
            glViewport(w, self.d, self.w, self.h)
            glLoadIdentity()
            glOrtho(0, self.w, self.h, 0, -1, 1)
        else:
            self.scorePanel.paintComponent()
        glutSwapBuffers()

    ## Fill a rectangle.
    #
    # @param x coordinate of the upper left corner.
    # @param y coordinate of the upper left corner.
    # @param w width of the rectangle.
    # @param h height of the rectangle.
    #
    def fillRect(self, x, y, w, h):
        glBegin(GL_QUADS)
        glVertex2f(x, y)
        glVertex2f(x + w, y)
        glVertex2f(x + w, y + w)
        glVertex2f(x, y + w)
        glEnd()

    ## Draw the edges of a rectangle.
    #
    # @param x coordinate of the upper left corner.
    # @param y coordinate of the upper left corner.
    # @param w width of the rectangle.
    # @param h height of the rectangle.
    #
    def drawRect(self, x, y, w, h):
        glBegin(GL_LINE_LOOP)
        glVertex2f(x, y)
        glVertex2f(x + w, y)
        glVertex2f(x + w, y + w)
        glVertex2f(x, y + w)
        glEnd()

    ## Line width.
    def setStroke(self, w):
        ## Line width.
        self.stroke = w
        glLineWidth(w)

    ## Set current color.
    def setColor(self, c):
        ## Current color.
        self.color = self.colors[c]
        glColor3f(*self.color)

    ##
    #  Maps a point from screen coordinates to WC.
    #  If the game viewport origin in not at (0,0), this is necessary.
    #
    #  @param x given point.
    #  @param y given point.
    #  @return pt in world coordinates.
    #
    def unProject(self, x, y):
        modelMatrix = glGetDoublev(GL_MODELVIEW_MATRIX)
        projMatrix = glGetDoublev(GL_PROJECTION_MATRIX)
        viewport = glGetIntegerv(GL_VIEWPORT)

        h = glutGet(GLUT_WINDOW_HEIGHT)
        y = h - y

        wcx, wcy, wcz = gluUnProject(x, y, 0, modelMatrix, projMatrix,
                                     viewport)

        return int(wcx), int(wcy)

    ## Callback for mouse button pressed.
    def mousePressed(self, button, button_state, cursor_x, cursor_y):
        if (self.flashingState > 0 or self.collapsing or self.filling): return

        if (button == GLUT_LEFT_BUTTON) and (button_state == GLUT_DOWN):
            cursor_x, cursor_y = self.unProject(cursor_x, cursor_y)
            row = cursor_y // GameMain.SIZE
            col = cursor_x // GameMain.SIZE
            if row >= self.game.getHeight() or col >= self.game.getWidth() or \
               row < 0 or col < 0:
                return
            if GameMain.__DEBUG__:
                print("(row,col) = (%d,%d)" % (row, col))
            self.currentCell = Cell(row, col, self.game.getIcon(row, col))
            glutPostRedisplay()
        elif (button == GLUT_LEFT_BUTTON) and (button_state == GLUT_UP):
            # If we have selected two adjacent cells, then tell the game to try
            # to swap them.
            if (self.currentCell is not None):
                if (self.nextCell is not None):
                    swapped = self.game.select(
                        [self.currentCell, self.nextCell])
                    if GameMain.__DEBUG__:
                        print(swapped)
                    if swapped:
                        # Successful move, start up the timer
                        self.timer.setDelay(self.flashingSpeed)
                        self.timer.restart()

            self.currentCell = None
            self.nextCell = None
            glutPostRedisplay()

    ## Callback for mouse button dragged.
    def mouseDragged(self, cursor_x, cursor_y):
        cursor_x, cursor_y = self.unProject(cursor_x, cursor_y)
        row = cursor_y // GameMain.SIZE
        col = cursor_x // GameMain.SIZE
        if row >= self.game.getHeight() or col >= self.game.getWidth() or \
           row < 0 or col < 0:
            return
        if self.currentCell is not None:
            # if the cell is adjacent to the one in which mouse was pressed,
            # record it as the nextCell
            c = Cell(row, col, self.game.getIcon(row, col))
            if self.currentCell.isAdjacent(c):
                self.nextCell = c
            else:
                self.nextCell = None
            glutPostRedisplay()

    ## The paintComponent is invoked whenever the panel needs
    # to be rendered on the screen. In this application,
    # repainting is normally triggered by the calls to the repaint()
    # method in the timer callback and the keyboard event handler (see below).
    def paintComponent(self):
        # clear background
        glClearColor(*self.backgroundColor)
        glClear(GL_COLOR_BUFFER_BIT)

        # paint occupied cells of the grid
        for row in range(self.game.getHeight()):
            for col in range(self.game.getWidth()):
                t = self.game.getIcon(row, col)
                if t is not None:
                    self.paintOneCell(row, col, t)

        if self.currentCell is not None:
            self.highlightOneCell(self.currentCell.row(),
                                  self.currentCell.col())

        if self.nextCell is not None:
            self.highlightOneCell(self.nextCell.row(), self.nextCell.col())

        if self.flashingState > 0:
            # need to paint the cells, since they are nulled out in game
            for p in self.cellsToCollapse:
                self.paintOneCell(p.row(), p.col(), p.getIcon())

            # if cells are collapsing, flash them
            if self.flashingState % 2 != 0:
                self.highlightOneCell(p.row(), p.col())

        if self.collapsing:
            # first grey out the column where cells are moving
            # in order to match the background
            for c in self.movingCells:
                col = c.col()
                row = c.row()
                self.paintOneCellBG(row, col, GamePanel.BACKGROUND_COLOR)

            for c in self.movingCells:
                col = c.col()
                pixel = c.currentPixel
                self.paintOneCellByPixel(pixel, col, c.getIcon())

        if self.filling:
            # first grey out the column where cells are moving
            for c in self.fillingCells:
                col = c.col()
                row = c.row()
                self.paintOneCellBG(row, col, GamePanel.BACKGROUND_COLOR)

            for c in self.fillingCells:
                col = c.col()
                pixel = c.currentPixel
                self.paintOneCellByPixel(pixel, col, c.getIcon())

    ##
    # Renders a single cell of the grid.
    #
    # @param row y-coordinate of the cell to render
    # @param col x-coordinate of the cell to render
    # @param t the icon to render, normally used
    #   to determine the color with which to render the cell
    #
    def paintOneCell(self, row, col, t):
        # scale everything up by the SIZE
        x = GameMain.SIZE * col
        y = GameMain.SIZE * row
        self.setColor(self.getColorForIcon(t))
        self.fillRect(x, y, GameMain.SIZE, GameMain.SIZE)
        self.setColor(GamePanel.BOUNDARY_COLOR)
        self.drawRect(x, y, GameMain.SIZE - 1, GameMain.SIZE - 1)

    ##
    # Renders a single cell of the grid.
    #
    # @param row y-coordinate of the cell to render
    # @param col x-coordinate of the cell to render
    # @param color the color to render
    #
    def paintOneCellBG(self, row, col, color):
        # scale everything up by the SIZE
        x = GameMain.SIZE * col
        y = GameMain.SIZE * row
        self.setColor(color)
        self.fillRect(x, y, GameMain.SIZE, GameMain.SIZE)
        self.setColor(GamePanel.BACKGROUND_COLOR)
        self.drawRect(x, y, GameMain.SIZE - 1, GameMain.SIZE - 1)

    ##
    # Renders a single cell of the grid specifying its vertical
    # position in pixels.
    #
    # @param rowPixel y-coordinate of pixel at which to render the cell
    # @param col x-coordinate of the cell to render
    # @param t the icon to render, normally used
    #   to determine the color with which to render the cell
    #
    def paintOneCellByPixel(self, rowPixel, col, t):
        # scale everything up by the SIZE
        x = GameMain.SIZE * col
        y = rowPixel
        self.setColor(self.getColorForIcon(t))
        self.fillRect(x, y, GameMain.SIZE, GameMain.SIZE)
        self.setColor(GamePanel.BOUNDARY_COLOR)
        self.drawRect(x, y, GameMain.SIZE - 1, GameMain.SIZE - 1)

    ##
    # Draws a white border around one cell.
    #
    # @param row y-coordinate of the cell to highlight
    # @param col x-coordinate of the cell to highlight
    #
    def highlightOneCell(self, row, col):
        self.setColor(Color.WHITE)
        self.setStroke(2)
        self.drawRect(col * GameMain.SIZE, row * GameMain.SIZE, GameMain.SIZE,
                      GameMain.SIZE)
        self.setStroke(1)

    ##
    # Listener for timer events.
    #
    # This is the display callback method,
    # which is invoked each time the timer fires and the call
    # to repaint() at the bottom of the method causes the panel
    # to be redrawn.
    #
    def TimerCallback(self):
        # State may be flashing, collapsing or filling.
        # Timer is not stopped until all cascading collapses are finished.

        if (self.flashingState == 0 and not self.collapsing
                and not self.filling):
            # Either we are just starting execution of the timer, or
            # have finished a previous collapse and fill and need to check
            # whether there is a cascading collapse. If so,
            # set the flashing state and fall through
            self.cellsToCollapse = self.game.findRuns(True)
            if len(self.cellsToCollapse) != 0:
                self.flashingState = self.numberOfFlashes * 2
                self.timer.setDelay(self.flashingSpeed)
                self.timer.restart()

                # update the score too
                self.scorePanel.updateScore(
                    self.game.getScore(), not self.game.debug
                    and self.scorePanel.g is None)
            else:
                # nothing more to collapse
                self.cellsToCollapse = None
                self.timer.stop()

        if (self.flashingState > 0):
            self.flashingState -= 1
            if (self.flashingState == 0):
                # Done flashing, start collapsing
                self.timer.setDelay(self.fallingSpeed)
                self.timer.restart()
                currentMovedCells = []
                for col in range(self.game.getWidth()):
                    currentMovedCells.extend(self.game.collapseColumn(col))
                # go into collapsing state
                self.collapsing = True
                self.movingCells = []
                for c in currentMovedCells:
                    self.movingCells.append(AnimatedCell(c))

        if self.collapsing:
            # see if there are still cells moving
            found = False
            for cell in self.movingCells:
                if not cell.done():
                    found = True
                    cell.animate(self.fallPerFrame)

            if not found:
                # done collapsing, start filling
                self.collapsing = False
                self.movingCells = None

                self.initializeCellsToFill()
                self.filling = True
                if len(self.fillingCells) == 0:
                    print(
                        "WARNING: game returned collapsing cells but failed to return cells to fill columns"
                    )
                    self.filling = False
                    self.fillingCells = None

        if self.filling:
            # see if we're done
            found = False
            for cell in self.fillingCells:
                if not cell.done():
                    found = True
                    cell.animate(self.fallPerFrame)

            if not found:
                self.filling = False
                self.fillingCells = None

        self.repaint()

    ##
    # Sets up the fillingCells list.
    #
    def initializeCellsToFill(self):
        self.fillingCells = []
        for col in range(self.game.getWidth()):
            currentNewCells = self.game.fillColumn(col)
            for c in currentNewCells:
                self.fillingCells.append(AnimatedCell(c, -1))

    ## Return the key corresponding to a certain value in a dictionary.
    get_key = lambda v, d: next(k for k in d if d[k] is v)

    ##
    # Returns a color for the given icon.
    # @param icon
    # @return
    #
    def getColorForIcon(self, icon):
        if icon is None: return Color.GRAY
        index = icon.getType()
        if (index < 0 or index > len(self.colors)):
            return Color.BLACK

        return Color(index)
class GamePanel:

    ## Background color.
    BACKGROUND_COLOR = Color.GRAY
    ## Border color.
    BOUNDARY_COLOR = Color.GREY

    ##
    # Constructs a GamePanel with the given game associated ScorePanel.
    # @param game
    #   the IGame instance for which this is the UI.
    # @param root
    #   root window.
    # @param canvas
    #   canvas for drawing.
    # @param scorePanel
    #   panel for displaying scores associated with the game.
    #
    def __init__(self, game, root, canvas, scorePanel=None):
        ## Number of pixels each icon falls per frame when animating.
        self.fallPerFrame = 9
        ## Interval between frames when animating falling icons, in milliseconds.
        self.fallingSpeed = 20
        ## Number of times to flash when a pair is selected.
        self.numberOfFlashes = 3
        ## Interval between flashes, in milliseconds.
        self.flashingSpeed = 50
        ## State variable counts down to zero while flashing the cells to be collapsed.
        self.flashingState = 0
        ## Cells about to be collapsed are flashed briefly before moving.
        self.cellsToCollapse = None
        ## Flag indicates whether we are currently moving cells down.
        self.collapsing = False
        ## Cells currently being moved down during a collapse.
        self.movingCells = None
        ## Flag indicates whether we are currently filling new cells.
        self.filling = False
        ## New cells filled from top.
        self.fillingCells = None
        ## Cell currently selected by a mouse down event.
        self.currentCell = None
        ## Cell currently selected by a mouse drag event.
        self.nextCell = None
        ## MainPanel.
        self.root = root
        ## Canvas.
        self.g = canvas
        ## Line width.
        self.setStroke(1)

        ## Dictionary of colors.
        self.colors = {
            Color.GRAY: 'gray',
            Color.GREY: 'grey',
            Color.RED: 'red',
            Color.GREEN: 'green2',
            Color.CYAN: 'cyan',
            Color.YELLOW: 'yellow',
            Color.WHITE: 'white',
            Color.BLACK: 'black',
            Color.ORANGE: 'orange'
        }

        ## The IGame instance for which this is the UI.
        self.game = game
        ## Score panel associated with the game.
        self.scorePanel = scorePanel

        self.g.bind("<Button-1>", self.mousePressed)
        self.g.bind("<ButtonRelease-1>", self.mouseReleased)
        self.g.bind("<B1-Motion>", self.mouseDragged)

        ## Timer instance used to animate the UI.
        self.timer = Timer(self.root, self.fallingSpeed, self.TimerCallback)
        self.timer.run()

    ## Force a repaint of this panel.
    def repaint(self):
        self.paintComponent(self.g)
        self.scorePanel.paintComponent()
        self.root.update_idletasks()

    ## Fill a rectangle.
    def fillRect(self, x, y, w, h):
        self.g.create_rectangle(x,
                                y,
                                x + w,
                                y + h,
                                outline='black',
                                fill=self.color)

    ## Draw a rectangle.
    def drawRect(self, x, y, w, h):
        self.g.create_rectangle(x,
                                y,
                                x + w,
                                y + h,
                                outline=self.color,
                                width=self.stroke)

    ## Line width.
    def setStroke(self, w):
        self.stroke = w

    ## Set current color.
    def setColor(self, c):
        self.color = self.colors[c]

    ## Callback for mouse button pressed.
    def mousePressed(self, event):
        if (self.flashingState > 0 or self.collapsing or self.filling): return

        row = event.y // GameMain.SIZE
        col = event.x // GameMain.SIZE
        if GameMain.__DEBUG__:
            print("(row,col) = (%d,%d)" % (row, col))
        self.currentCell = Cell(row, col, self.game.getIcon(row, col))
        self.repaint()

    ## Callback for mouse button released.
    def mouseReleased(self, event):
        if (self.flashingState > 0 or self.collapsing or self.filling): return

        # If we have selected two adjacent cells, then tell the game to try
        # to swap them.
        if (self.currentCell is not None):
            if (self.nextCell is not None):
                swapped = self.game.select([self.currentCell, self.nextCell])
                if GameMain.__DEBUG__:
                    print(swapped)
                self.repaint()
                if swapped:
                    # Successful move, start up the timer
                    self.timer.setDelay(self.flashingSpeed)
                    self.timer.restart()

        self.currentCell = None
        self.nextCell = None

    ## Callback for mouse button dragged.
    def mouseDragged(self, e):
        row = e.y // GameMain.SIZE
        col = e.x // GameMain.SIZE
        if self.currentCell is not None:
            # if the cell is adjacent to the one in which mouse was pressed,
            # record it as the nextCell
            c = Cell(row, col, self.game.getIcon(row, col))
            if self.currentCell.isAdjacent(c):
                self.nextCell = c
            else:
                self.nextCell = None
            self.repaint()

    ## The paintComponent is invoked whenever the panel needs
    # to be rendered on the screen. In this application,
    # repainting is normally triggered by the calls to the repaint()
    # method in the timer callback and the keyboard event handler (see below).
    def paintComponent(self, g):
        # clear background
        self.setColor(GamePanel.BACKGROUND_COLOR)
        self.fillRect(0, 0,
                      self.game.getWidth() * GameMain.GRID_SIZE,
                      self.game.getHeight() * GameMain.GRID_SIZE)

        # paint occupied cells of the grid
        for row in range(self.game.getHeight()):
            for col in range(self.game.getWidth()):
                t = self.game.getIcon(row, col)
                if t is not None:
                    self.paintOneCell(g, row, col, t)

        if self.currentCell is not None:
            self.highlightOneCell(g, self.currentCell.row(),
                                  self.currentCell.col())

        if self.nextCell is not None:
            self.highlightOneCell(g, self.nextCell.row(), self.nextCell.col())

        if self.flashingState > 0:
            # need to paint the cells, since they are nulled out in game
            for p in self.cellsToCollapse:
                self.paintOneCell(g, p.row(), p.col(), p.getIcon())

            # if cells are collapsing, flash them
            if self.flashingState % 2 != 0:
                self.highlightOneCell(g, p.row(), p.col())

        if self.collapsing:
            # first grey out the column where cells are moving
            # in order to match the background
            for c in self.movingCells:
                col = c.col()
                row = c.row()
                self.paintOneCellBG(g, row, col, GamePanel.BACKGROUND_COLOR)

            for c in self.movingCells:
                col = c.col()
                pixel = c.currentPixel
                self.paintOneCellByPixel(g, pixel, col, c.getIcon())

        if self.filling:
            # first grey out the column where cells are moving
            for c in self.fillingCells:
                col = c.col()
                row = c.row()
                self.paintOneCellBG(g, row, col, GamePanel.BACKGROUND_COLOR)

            for c in self.fillingCells:
                col = c.col()
                pixel = c.currentPixel
                self.paintOneCellByPixel(g, pixel, col, c.getIcon())

    ##
    # Renders a single cell of the grid.
    #
    # @param g graphics context
    # @param row y-coordinate of the cell to render
    # @param col x-coordinate of the cell to render
    # @param t the icon to render, normally used
    #   to determine the color with which to render the cell
    #
    def paintOneCell(self, g, row, col, t):
        # scale everything up by the SIZE
        x = GameMain.SIZE * col
        y = GameMain.SIZE * row
        self.setColor(self.getColorForIcon(t))
        self.fillRect(x, y, GameMain.SIZE, GameMain.SIZE)
        self.setColor(GamePanel.BOUNDARY_COLOR)
        self.drawRect(x, y, GameMain.SIZE - 1, GameMain.SIZE - 1)

    ##
    # Renders a single cell of the grid.
    #
    # @param g graphics context
    # @param row y-coordinate of the cell to render
    # @param col x-coordinate of the cell to render
    # @param color the color to render
    #
    def paintOneCellBG(self, g, row, col, color):
        # scale everything up by the SIZE
        x = GameMain.SIZE * col
        y = GameMain.SIZE * row
        self.setColor(color)
        self.fillRect(x, y, GameMain.SIZE, GameMain.SIZE)
        self.setColor(GamePanel.BACKGROUND_COLOR)
        self.drawRect(x, y, GameMain.SIZE - 1, GameMain.SIZE - 1)

    ##
    # Renders a single cell of the grid specifying its vertical
    # position in pixels.
    #
    # @param g graphics context
    # @param rowPixel y-coordinate of pixel at which to render the cell
    # @param col x-coordinate of the cell to render
    # @param t the icon to render, normally used
    #   to determine the color with which to render the cell
    #
    def paintOneCellByPixel(self, g, rowPixel, col, t):
        # scale everything up by the SIZE
        x = GameMain.SIZE * col
        y = rowPixel
        self.setColor(self.getColorForIcon(t))
        self.fillRect(x, y, GameMain.SIZE, GameMain.SIZE)
        self.setColor(GamePanel.BOUNDARY_COLOR)
        self.drawRect(x, y, GameMain.SIZE - 1, GameMain.SIZE - 1)

    ##
    # Draws a white border around one cell.
    #
    # @param g graphics context
    # @param row y-coordinate of the cell to highlight
    # @param col x-coordinate of the cell to highlight
    #
    def highlightOneCell(self, g, row, col):
        self.setColor(Color.WHITE)
        self.setStroke(2)
        self.drawRect(col * GameMain.SIZE, row * GameMain.SIZE, GameMain.SIZE,
                      GameMain.SIZE)
        self.setStroke(1)

    ##
    # Listener for timer events.  The actionPerformed method
    # is invoked each time the timer fires and the call to
    # repaint() at the bottom of the method causes the panel
    # to be redrawn.
    #
    def TimerCallback(self):
        # State may be flashing, collapsing or filling.
        # Timer is not stopped until all cascading collapses are finished.

        if (self.flashingState == 0 and not self.collapsing
                and not self.filling):
            # Either we are just starting execution of the timer, or
            # have finished a previous collapse and fill and need to check
            # whether there is a cascading collapse. If so,
            # set the flashing state and fall through
            self.cellsToCollapse = self.game.findRuns(True)
            if len(self.cellsToCollapse) != 0:
                self.flashingState = self.numberOfFlashes * 2
                self.timer.setDelay(self.flashingSpeed)
                self.timer.restart()

                # update the score too
                if self.scorePanel is not None:
                    self.scorePanel.updateScore(self.game.getScore())
            else:
                # nothing more to collapse
                self.cellsToCollapse = None
                self.timer.stop()

        if (self.flashingState > 0):
            self.flashingState -= 1
            if (self.flashingState == 0):
                # Done flashing, start collapsing
                self.timer.setDelay(self.fallingSpeed)
                self.timer.restart()
                currentMovedCells = []
                for col in range(self.game.getWidth()):
                    currentMovedCells.extend(self.game.collapseColumn(col))
                # go into collapsing state
                self.collapsing = True
                self.movingCells = []
                for c in currentMovedCells:
                    self.movingCells.append(AnimatedCell(c))

        if self.collapsing:
            # see if there are still cells moving
            found = False
            for cell in self.movingCells:
                if not cell.done():
                    found = True
                    cell.animate(self.fallPerFrame)

            if not found:
                # done collapsing, start filling
                self.collapsing = False
                self.movingCells = None

                self.initializeCellsToFill()
                self.filling = True
                if len(self.fillingCells) == 0:
                    print(
                        "WARNING: game returned collapsing cells but failed to return cells to fill columns"
                    )
                    self.filling = False
                    self.fillingCells = None

        if self.filling:
            # see if we're done
            found = False
            for cell in self.fillingCells:
                if not cell.done():
                    found = True
                    cell.animate(self.fallPerFrame)

            if not found:
                self.filling = False
                self.fillingCells = None

        self.repaint()

    ##
    # Sets up the fillingCells list.
    #
    def initializeCellsToFill(self):
        self.fillingCells = []
        for col in range(self.game.getWidth()):
            currentNewCells = self.game.fillColumn(col)
            for c in currentNewCells:
                self.fillingCells.append(AnimatedCell(c, -1))

    ## Return the key corresponding to a certain value in a dictionary.
    get_key = lambda v, d: next(k for k in d if d[k] is v)

    ##
    # Returns a color for the given icon.
    # @param icon
    # @return
    #
    def getColorForIcon(self, icon):
        if icon is None: return Color.GRAY
        index = icon.getType()
        if (index < 0 or index > len(self.colors)):
            return Color.BLACK

        return Color(index)