首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >2048游戏用Python编写

2048游戏用Python编写
EN

Code Review用户
提问于 2018-02-19 18:44:29
回答 1查看 9.3K关注 0票数 11

这是我的代码在以自动教学的方式学习Python两年之后第一次被审查。我从来没有上过一门课,也没上过课,但我只在youtube上读过书和看过一些视频。

我从零开始重写了2048游戏,试图使代码更具可读性和表现性。

Cell.py

代码语言:javascript
复制
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

代码语言:javascript
复制
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()
EN

回答 1

Code Review用户

发布于 2018-02-20 07:16:21

可玩性

对于一个2048年的游戏,我希望至少所有的瓷砖到2048年都会有不同的颜色。

每一个动作的动画都非常烦人和令人困惑:

  • 滑动运动通常太慢,但有时速度很快。
  • 两个相等的瓷砖碰撞的结果应该会导致合并的瓷砖,但是合并的瓷砖会在滑动动画完成之前立即出现。
  • 随机的新瓷砖通常在滑动完成之前出现。这常常导致瓷砖在新瓷砖下面滑动。

当一个新的瓷砖出现时,它的值为2、4或8,概率相等。在一个真实的2048年游戏中,它通常是2,偶尔是4,但从来没有8。

“游戏结束”条件应尽快检测到董事会是满的,不能移动是可能的。您不应该等到用户尝试下一步时才行动。

Implementation

Cell类的名称是错误的;它的名称似乎是指董事会的一个固定位置,但实际上是指一个可移动的项目。在英语中,Tile将是一个更合适的名称。

对每个Cell对象使用一个UUID似乎有点过火。从全局计数器(可能是从id )生成itertools.count()就足够了。

向左、右、向下和向上映射数字0、1、2和3,让App.callback()将这些数字映射回._left()._right()._down()._up()方法是对魔术数字的毫无意义的使用。您可以将这四个方向表示为字符串,并根据名称执行方法查找。

这四种向每个方向移动细胞的方法是非常相似的。应该重构它们,以调用接受(Δx,Δy)作为参数的公共处理程序。

票数 10
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/187895

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档