2048 игра, написанная в Python


enter image description here

Это первый раз, когда мой код пересматривается после двух лет обучения Python в качестве самоучки. Я никогда не взял курс или класс, но я только читал книги и видел несколько видео на YouTube.

Я переписал с нуля 2048 игры, пытаясь сделать код более читабельным и эффективным можно.

Cell.py

from uuid import uuid4
from collections import defaultdict

WAIT = 1  # ms between frames

COLOR_TABLE = defaultdict(lambda: "#ffffff",
                          **{"2": "#f4e242", "4": "#f4b841", "8": "#f49d41", "16": "#f48241",
                             "32": "#f46741", "64": "#f45541", "128": "#f44141", "256": "#f4416d"})


def sign(n):
    """ Return the sign of a number """

    if n > 0:
        return 1
    elif n < 0:
        return -1
    return 0


class Cell:
    def __init__(self, canvas, root, pos, l, n=2):
        self.canvas = canvas
        self.root = root
        self.n = n  # Number to display
        self.pos = tuple(pos)  # Position on the table as (x, y)
        self.l = l  # Side leght
        self.font = ("Helvetica", int(self.l / 3))
        self.id = str(uuid4())  # Personal id of every cell

        self._draw()

    def move(self, x, y):
        """ Function called by the user to move the cell of (x, y) position (the grid is 4 positions wide """

        if x != 0 and y != 0:
            return  # It can't move diagonally

        self._moveloop(x * self.l, y * self.l)

    def double(self):
        self.n *= 2
        self.canvas.itemconfig(self.id + "text", text=self.n)
        self.canvas.itemconfig(self.id + "cell", fill=COLOR_TABLE[str(self.n)])

    def _draw(self):
        """ Draws the cell and his number on the canvas"""

        x, y = self.pos

        self.canvas.create_rectangle(x * self.l, y * self.l, (x + 1) * self.l, (y + 1) * self.l, fill=COLOR_TABLE[str(self.n)],
                                     tag=(self.id, self.id + "cell"))
        self.canvas.create_text(x * self.l + (self.l / 2), y * self.l + (self.l / 2), text=self.n, font=self.font, tag=(self.id, self.id + "text"))

    def _moveloop(self, tomovex, tomovey):
        """ Recursive function that moves the cell 1px each call """

        if not tomovex and not tomovey:
            return  # Break the loop

        self.canvas.move(self.id, sign(tomovex), sign(tomovey))
        newx = (abs(tomovex) - 1) * sign(tomovex)
        newy = (abs(tomovey) - 1) * sign(tomovey)

        self.root.after(WAIT, lambda: self._moveloop(newx, newy))

    def __del__(self):
        """ When the cell is overwritten his canvas elements must be deleted """

        self.canvas.tag_lower(self.id)
        self.root.after(int(self.l * 4), lambda: self.canvas.delete(self.id))

    def __repr__(self):
        """ Debug purpose """

        return "C(%s)" % self.n

Main.py

from tkinter import *
from random import randint as ri
from copy import copy

import cell

WIDTH = 400


class App:
    def __init__(self, parent):

        self.root = parent

        # Array where all the cells are saved
        self.table = [0] * 16
        # Boolean to control user inputs to avoid too many clicks
        self._canclick = True
        # Score
        self._score = 0

        # Draws all the tkinter elements
        self.main_canvas = Canvas(self.root, width=WIDTH, height=WIDTH, bg="lightblue")
        self.main_canvas.pack()
        self.second_frame = Frame(self.root)
        self.second_frame.pack()
        self._scorevar = StringVar()
        self._scorevar.set(self._score)
        self._sframesetup()

        # Draws the horizontal and vertical lines
        self._griddraw()

        # Draws the cells
        self._cellsetup(3)

    def callback(self, direction):
        """ Handles the user input

            direction: LEFT, RIGHT, DOWN, UP = 0, 1, 2, 3"""
        if self._canclick:
            self._canclick = False  # Blocks the user input

            # Makes a copy of the table to check later if something changed or not
            backup = copy(self.table)

            d = dict(enumerate([self._left, self._right, self._down, self._up]))
            d[direction]()  # Calls the right movement function

            # Check if there is space to spawn a new cell
            if not 0 in self.table:
                self._lose()
                return

            if backup != self.table:
                # Waits until the cells stop and spawns a new one
                self.root.after(301, self._spawnnew)
            else:
                self._canclick = True

    def restart(self):
        """ Restart button callback """

        # deletes lose text
        self.main_canvas.delete("d72819d9")

        # deletes all cells
        self.table = [0] * 16

        self._cellsetup(3)
        self._scorevar.set(0)

    def _sframesetup(self):
        pointframe = Frame(self.second_frame)
        pointframe.pack(side=LEFT, pady=20, padx=20)

        Label(pointframe, text="Punteggio:").pack(side=LEFT)
        Label(pointframe, textvariable=self._scorevar).pack(side=LEFT)

        restartb = Button(self.second_frame, text="Restart", command=self.restart)
        restartb.pack(side=RIGHT, pady=20, padx=20)

    def _griddraw(self):
        """ Draws the horizontal and vertical lines """

        line_color = "blue"
        cell_width = WIDTH / 4

        for n in range(1, 4):
            # Vertical lines
            self.main_canvas.create_line(n * cell_width, 0, n * cell_width, WIDTH, fill=line_color)
            # Horizontal lines
            self.main_canvas.create_line(0, n * cell_width, WIDTH, n * cell_width, fill=line_color)

    def _cellsetup(self, nstart):
        """ Spawns 'nstart' new cells and draws them """

        for _ in range(nstart):
            self._spawnnew()

    def _lose(self):
        """ Function called when the user is not able to continue the game """

        self.main_canvas.create_text(WIDTH / 2, WIDTH / 2, text="GAME OVER", font=("Helvetica", 25), tag="d72819d9")

    def _spawnnew(self):
        """ Creates a new cell and draws it in an empty space """

        while True:
            pos = ri(0, 15)  # Pick a random idx
            if self.table[pos]:
                # If the position is already taken, restart the loop
                continue

            posconv = pos % 4, int(pos / 4)  # Converts the new idx into (x, y)

            self.table[pos] = cell.Cell(self.main_canvas, self.root, posconv, WIDTH / 4, n=2 ** ri(1, 3))
            break

        # Let the user be able to click again
        self._canclick = True

    def _right(self):
        """ Moves all the cells to the right side """

        for idx in list(range(len(self.table)))[::-1]:  # Iterates the array backwards

            if self.table[idx]:  # Checks if there's a cell

                c = self.table[idx]  # Saves the cell because 'idx' will change later
                while (idx + 1) % 4:  # Keeps going till it reaches the left side

                    # 1° CASE: Two cells add up
                    if self.table[idx + 1] and self.table[idx + 1].n == self.table[idx].n:
                        self.table[idx + 1].double()  # Doubles a cell
                        self._scorevar.set(int(self._scorevar.get()) + self.table[idx + 1].n)  # Updates the score label
                        self.table[idx] = 0  # Deletes the other
                        idx += 1
                        break

                    # 2° CASE: The cell stops
                    elif self.table[idx + 1]:
                        break

                    # 3° CASE: The cell moves to the left
                    else:
                        self.table[idx + 1] = self.table[idx]
                        self.table[idx] = 0
                        idx += 1

                # Updates the canvas object of the cell and his '.pos' attribute
                newx, newy = idx % 4, int(idx / 4)
                c.move(newx - c.pos[0], newy - c.pos[1])
                c.pos = newx, newy

    def _left(self):
        """ Moves all the cells to the left side """

        for idx in range(len(self.table)):  # # Iterates the array from the beginning

            if self.table[idx]:

                c = self.table[idx]
                while idx % 4:

                    if self.table[idx - 1] and self.table[idx].n == self.table[idx - 1].n:
                        self.table[idx - 1].double()
                        self._scorevar.set(int(self._scorevar.get()) + self.table[idx - 1].n)
                        self.table[idx] = 0
                        idx -= 1
                        break

                    elif self.table[idx - 1]:
                        break

                    else:
                        self.table[idx - 1] = self.table[idx]
                        self.table[idx] = 0
                        idx -= 1

                newx, newy = idx % 4, int(idx / 4)
                c.move(newx - c.pos[0], newy - c.pos[1])
                c.pos = newx, newy

    def _down(self):
        """ Moves all the cells to the bottom """

        for idx in list(range(len(self.table)))[::-1]:

            if self.table[idx]:

                c = self.table[idx]
                while not 12 <= idx <= 15:

                    if self.table[idx + 4] and self.table[idx + 4].n == self.table[idx].n:
                        self.table[idx + 4].double()
                        self._scorevar.set(int(self._scorevar.get()) + self.table[idx + 4].n)
                        self.table[idx] = 0
                        idx += 4
                        break

                    elif self.table[idx + 4]:
                        break

                    else:
                        self.table[idx + 4] = self.table[idx]
                        self.table[idx] = 0
                        idx += 4

                newx, newy = idx % 4, int(idx / 4)
                c.move(newx - c.pos[0], newy - c.pos[1])
                c.pos = newx, newy

    def _up(self):
        """ Moves all the cells to the top """

        for idx in range(len(self.table)):

            if self.table[idx]:

                c = self.table[idx]
                while not 0 <= idx <= 3:

                    if self.table[idx - 4] and self.table[idx - 4].n == self.table[idx].n:
                        self.table[idx - 4].double()
                        self._scorevar.set(int(self._scorevar.get()) + self.table[idx - 4].n)
                        self.table[idx] = 0
                        idx -= 4
                        break

                    elif self.table[idx - 4]:
                        break

                    else:
                        self.table[idx - 4] = self.table[idx]
                        self.table[idx] = 0
                        idx -= 4

                newx, newy = idx % 4, int(idx / 4)
                c.move(newx - c.pos[0], newy - c.pos[1])
                c.pos = newx, newy


root = Tk()

app = App(root)

root.bind("<a>", lambda event: app.callback(0))
root.bind("<d>", lambda event: app.callback(1))
root.bind("<w>", lambda event: app.callback(3))
root.bind("<s>", lambda event: app.callback(2))

root.bind("<r>", lambda event: app.restart())

root.mainloop()


2460
11
задан 19 февраля 2018 в 06:02 Источник Поделиться
Комментарии
1 ответ

Играбельность

Для игры в 2048, я бы ожидать, что по крайней мере все плитки до 2048 будет иметь различные цвета.

Анимации на каждый ход-это очень раздражает и сбивает с толку:


  • Скользящим движением, как правило, слишком медленно, а иногда быстро.

  • В результате две одинаковые фишки, сталкиваясь в результате слияния плитка, но объединены плиточный появляется мгновенно, прежде чем раздвижные анимации.

  • Случайный новую плитку обычно появляется перед раздвижными завершена. Это часто приводит в плитке сползать "под" новую плитку.

Когда появится новая плитка, она имеет значение 2, 4 или 8 с равной вероятностью. В подлинной игре 2048, это обычно 2, а иногда 4, но не 8.

"Игра за" состояние должны быть обнаружены как только совет и не двигаться можно. Вы не должны ждать, пока пользователь пытается сделать следующий шаг.

Реализация

В Cell класс-это неправильное название, его имя, кажется, относятся к одной из фиксированных позиций совета, но на самом деле относится к одному из подвижных элементов. На английском языке, Tile было бы более подходящее название.

Используя UUID для каждого Cell объект, кажется, как Overkill. Создание id из глобального счетчика (возможно, от itertools.count()) было бы достаточно.

Сопоставление влево, вправо, вниз и вверх к цифрам 0, 1, 2, и 3, соответственно, и App.callback() сопоставьте эти цифры вернуться к методам ._left(), ._right(), ._down()и ._up() это бессмысленно использование магических чисел. Ты мог бы представить в четырех направлениях, как струны, и выполняется поиск метода, основанного на имени.

Эти четыре метода для перемещения клеток в каждом направлении чрезвычайно похожи друг на друга. Они должны быть переработаны, чтобы вызвать общий обработчик, который принимает (ДХ, Δy) в качестве параметров.

10
ответ дан 20 февраля 2018 в 07:02 Источник Поделиться