python遍历树_用Python遍历树解决纸Mario环拼图

python遍历树

Paper Mario has always been one of my favorite series and I’ve had a blast playing through and beating the newest installment for Nintendo Switch, Paper Mario: The Origami King. While I thought the story, riddles, and hidden secrets were great, I found the main combat mechanic to be one of the bleakest parts of the game.

Paper Mario一直是我最喜欢的系列之一,在Nintendo Switch的最新作品《 Paper Mario:折纸之王》中,我一直玩得很尽兴。 当我认为故事,谜语和隐藏的秘密很棒时,我发现主要的战斗技工是游戏中最惨淡的部分之一。

Part of my frustration definitely came from my belief that some of these ring puzzles were unsolvable, so part of the intent of this mini-project was to validate whether or not that was true. The result was a Python script that produces the necessary steps to get a matching lineup every time. I explain the algorithm in detail throughout this article, but it can get technical.

我之所以感到沮丧,一定是因为我相信其中的一些难题是无法解决的,因此,这个小型项目的部分目的是验证这是否成立。 结果是一个Python脚本,该脚本生成了必要的步骤以每次都获得匹配的阵容。 我将在本文中详细解释该算法,但可以从技术上获得帮助。

If you’re just interested in using the script, I’d recommend you first read the ‘Matrix Representation’ section first before skipping to the ‘Using this Script’ section. For reference, the GitHub repository is also linked here.

如果您只是对使用脚本感兴趣,建议您先阅读“矩阵表示”部分,然后再跳到“使用此脚本”部分。 作为参考,GitHub存储库也链接到此处

背景 (Background)

The main combat mechanic of Origami King is arranging enemies in groups of 4 on a ring puzzle board. The ring puzzle board consists of 5 concentric circles. 4 of these circles create rings that are rotatable clockwise or counterclockwise. Each of these circles is also divided into 12 slices that can be shifted back and forth.

Origami King的主要战斗技工是在环形拼图板上以4人一组的形式安排敌人。 环形拼图板由5个同心圆组成。 这些圆圈中的4个会产生可顺时针或逆时针旋转的 。 这些圆圈中的每一个也分为12个切片 ,可以来回移动。

Slices that are directly opposite from one another are connected (like a circular doubly linked list in CS). I’ve attached a GIF below that illustrates this better than I can explain it.

彼此直接相对的切片已连接(例如CS中的圆形双向链表)。 我在下面附加了一个GIF,它比我可以解释的更好。

Enemies should be arranged in one of two ways. The first pattern is when all 4 enemies occupy the same slice. The second pattern is when enemies occupy a 2x2 grid directly in front of Mario. The figure below displays these two patterns.

应该以两种方式之一来安排敌人。 第一种模式是当所有四个敌人占据相同的切片时。 第二种模式是当敌人在马里奥前直接占据2x2网格时。 下图显示了这两种模式。

Image for post
Two types of attack patterns to arrange enemies highlighted in red.
两种类型的攻击模式来布置以红色突出显示的敌人。

Each puzzle has a certain number of “ring moves” that these patterns need to be achieved in. In my play through, I have never observed a puzzle that needed more than 3 ring moves.

每个难题都有一定数量的“环动”,需要实现这些模式。在我的游戏中,我从未观察到需要超过3次环动的难题。

算法总结 (Summary of Algorithm)

I use a tree data structure to solve each puzzle. Starting from the initial state of the puzzle board, I generate a list of valid moves. Each move is added as a child node to the current board state. For each child, I will generate more valid moves and continue adding them as children for the number of ring moves that there are in the game. This process is illustrated in the figure below.

我使用树数据结构来解决每个难题。 从拼图板的初始状态开始,我会生成有效动作列表。 每一步都作为子节点添加到当前板状态。 对于每个孩子,我将生成更多有效动作,并继续将它们添加为孩子,以增加游戏中的环动作数量。 下图说明了此过程。

Image for post

At the end, I’ll validate all the children of the tree at the depth of the ring move, and return the first one that has a winning game state.

最后,我将在环移动深度验证树的所有子代,并返回第一个具有获胜游戏状态的子代。

矩阵表示 (Matrix Representation)

I represent the game state as a matrix. Each ring becomes a row and each slice of that ring acts as a column, creating a 4x12 matrix. The figure below shows how I numbered each of the rings and slices.

我将游戏状态表示为矩阵。 每个环变成一行,该环的每个切片充当一列,创建一个4x12矩阵。 下图显示了如何对每个圆环和切片进行编号。

Image for post
Rings are numbered from out to in; indexing starts at 0. Slices are numbered clockwise starting from the left most side (sorry if I triggered you by not numbering them like polar coordinates).
环的编号是从外到内。 索引从0开始。切片从最左侧开始按顺时针编号(很抱歉,如果我通过不像极坐标那样编号它们来触发了您)。

Each cell of the matrix receives a value of 0 or 1. A value of 0 means that the cell does not contain an enemy, while a value of 1 means that the cell does. In the following figures, I walk through an example containing actual gameplay.

矩阵的每个像元接收值为0或1。值0表示该像元不包含敌人,而值1表示该像元包含敌人。 在下图中,我将通过一个包含实际游戏玩法的示例进行介绍。

Image for post
Real gameplay example. Coordinates for each enemy are provided for reference.
真实的游戏示例。 提供每个敌人的坐标以供参考。
Image for post
Graphical representation of the enemies as 1’s in the matrix.
矩阵中敌人的图形表示为1。
Image for post
Matrix representation of the previous figure. Note that rings are the rows and slices are the columns.
上图的矩阵表示。 请注意,环是行,切片是列。

旋转和移位方法 (Spinning and Shifting Methods)

Spinning modifies the rows of the matrix by moving each cell a certain number of spaces to the right. In the graphical representation, this means that the rings spin clockwise. For simplicity’s sake, I never move the cells to the left or do a counterclockwise spin. Like a circular linked list, the element at the end becomes the first element of the list.

旋转通过将每个单元格向右移动一定数量的空格来修改矩阵的行。 在图形表示中,这意味着环顺时针旋转。 为简单起见,我从不将单元格向左移动或进行逆时针旋转。 与循环链表一样,末尾的元素成为列表的第一个元素。

Image for post
Visual representation of the matrix after the outermost ring is shifted one space.
最外环移动一格后矩阵的可视表示。
# arguments: (integer, integer, matrix)
# returns a matrix that has the specified ring spun the number of rotations
def spinRing(ring, rotations, puzzle):
    # print(type(puzzle))
    if not isinstance(puzzle, list):
        puzzle = puzzle.tolist()
    a = rotations % len(puzzle[ring])
    puzzle[ring] = puzzle[ring][-a:] + puzzle[ring][:-a]
    return puzzle

Shifting modifies the columns of the matrix. In the matrix representation, this means that columns 0 to 5 move downwards, but columns 6 to 11 actually move upwards. This is because each column from 0 to 5 is linked with a column 6 indices away from it. In the visual representation we are shifting slice #0 by 1 space. This causes slice #6 to shift upwards by 1 space.

移位修改矩阵的列。 在矩阵表示中,这意味着列0到5向下移动,但是列6到11实际上向上移动。 这是因为从0到5的每一列都与远离它的6个索引的列链接。 在视觉表示中,我们将切片#0移位了1个空间。 这导致切片#6向上移动1个空格。

Image for post
Before and after for shifting slice #0. The red boxes are grouped together. Note that they are 6 columns apart.
在移动切片#0之前和之后。 红色框组合在一起。 请注意,它们相距6列。

The code for shifting is a little more involved. First, the matrix is transposed so that the columns become rows. Next, each row past index 5 is reversed and appended to a row 6 indices above it. The shifting operation is then performed similarly to the spin operation. Finally, all the transformations are undone.

转移的代码要复杂得多。 首先,对矩阵进行转置,以使列变为行。 接下来,将越过索引5的每一行反转并附加到其上方的行6索引中。 然后,与旋转操作类似地执行移位操作。 最后,所有转换都将撤消。

# arguments: (integer, integer, matrix)
# returns a matrix that has the specified slice shifted the number of shifts
def shiftSlice(pieslice, shifts, puzzle):
    # transpose the array to sort by column
    transposePuzzle = np.transpose(puzzle).tolist()
    newPuzzle = []
    # combine the columns that are directly opposite each other
    for i in range(0, 6):
        newPuzzle.append(transposePuzzle[i] + list(reversed(transposePuzzle[i + 6])))
    # print np.array(newPuzzle)


    # do the shifting
    a = shifts % len(newPuzzle[pieslice])
    newPuzzle[pieslice] = newPuzzle[pieslice][-a:] + newPuzzle[pieslice][:-a]
    # print np.array(newPuzzle)


    # split the combined columns and rearrange
    for i in range(0, 6):
        transposePuzzle[i] = newPuzzle[i][0:4]
    for i in range(6, 12):
        transposePuzzle[i] = list(reversed(newPuzzle[i - 6][4:8]))
    # print np.array(transposePuzzle)
    return np.transpose(transposePuzzle)

检查胜利状态 (Checking Win State)

Recall that there are two types of win patterns to check for. The first pattern is when enemies are aligned in the same slice. I call this the lineup check. The second win pattern is when enemies occupy a 2x2 grid adjacent to the center of the board. I call this the hammer check because Mario can defeat enemies of this pattern using his hammer weapon.

回想一下,有两种类型的获胜模式需要检查。 第一种模式是敌人在同一切片中对齐时。 我称其为阵容检查。 第二种获胜方式是当敌人占据靠近棋盘中心的2x2网格时。 我称此为锤子检查,因为马里奥可以使用锤子武器击败这种模式的敌人。

For (almost) every enemy on the board, I check whether it belongs to a pattern of enemies that is winning. If the enemy does belong to a winning pattern, I clear that entire pattern of enemies from the board and keep iterating until I reach the position of another enemy.

对于(几乎)棋盘上的每个敌人,我检查它是否属于获胜的敌人模式。 如果敌人确实属于获胜模式,那么我将从棋盘上清除整个敌人模式,并不断进行迭代,直到我到达另一个敌人的位置为止。

# arguments: matrix
# returns a boolean to whether the board state is solved or unsolved
def checkWin(puzzle):
    puzzleCopy = copy.deepcopy(puzzle)
    for row in range(0, len(puzzleCopy)):
        for col in range(0, len(puzzleCopy[row])):
            if puzzleCopy[row][col] == 1:
                if not hammerCheck(row, col, puzzleCopy) and not lineupCheck(col, puzzleCopy):
                    return False
    return True

The lineup check looks within a specific slice and adds all values of that column to a separate list. The list is then checked to see if it contains all 1’s, representing all enemies. If the list does only contain enemies, then those enemies are removed from the puzzle board and a value of True is returned. Otherwise, the puzzle fails the lineup check.

阵容检查在特定切片中查找,并将该列的所有值添加到单独的列表中。 然后检查列表以查看其是否包含全1,代表所有敌人。 如果列表仅包含敌人,则将这些敌人从拼图板上删除,并返回值True。 否则,拼图无法通过阵容检查。

# arguments: (integer, matrix)
# returns a boolean to whether the enemy is eligible for a lineup attack
def lineupCheck(col, puzzle):
    # check if the column is filled with enemies
    enemiesArray = []
    for i in range(0, len(puzzle)):
        enemiesArray.append(puzzle[i][col])
    if enemiesArray.count(enemiesArray[0]) == len(enemiesArray) and enemiesArray[0] == 1:
        # remove enemies from the board and return true
        for i in range(0, len(puzzle)):
            puzzle[i][col] = 0
        return True
    return False

The hammer check is much more involved than the lineup check. I perform the following procedure:

锤子检查比阵容检查要复杂得多。 我执行以下过程:

  1. Check if the input row is index 2. Enemies occupying indices less than two are too far, and enemies at index 3 should have been eliminated already if they passed a hammer check previously.

    检查输入行是否为索引2。占据索引小于2的敌人距离太远,如果索引3的敌人先前通过了锤子检查,则应该已经将它们消除。
  2. Check if the enemy on row 2 also have enemies positioned directly below them.

    检查第2行上的敌人是否也有直接位于其下方的敌人。
  3. Check if enemies occupy the adjacent spaces on the same ring. This means that I check the left and right side of the current space. If an enemy does not exist to the left, then I verify that it must exist one space to the right, and one space to the right and below. The same logic applies mirrored if an enemy does not exist to the right.

    检查敌人是否占领同一环上的相邻空间。 这意味着我检查当前空间的左侧和右侧。 如果左边没有一个敌人,那么我确认它必须在右边存在一个空间,而在右边和下方必须存在一个空间。 如果右边不存在敌人,则采用相同的逻辑镜像。
# arguments: (integer, integer, matrix)
# returns a boolean to whether an enemy is eligible for a hammer combo
def hammerCheck(row, col, puzzle):
    # first check to see if the enemy is positioned on row 2
    if row == 2:
        if puzzle[3][col] == 0:  # enemies on row 2 must have enemies directly positioned below them
            return False


        if col < 11:
            # enemies must be adjacent to each other on the same row
            if puzzle[2][col - 1] == 0:  # if there is not an enemy to the left, then it must be on the right, and below
                if puzzle[2][col + 1] == 1 and puzzle[3][col + 1] == 1:  # remove enemies from board and return true
                    puzzle[2][col] = 0
                    puzzle[3][col] = 0
                    puzzle[2][col + 1] = 0
                    puzzle[3][col + 1] = 0
                    return True
                return False
            if puzzle[2][col + 1] == 0:  # if there is not an enemy to the right, then it must be on the left, and below
                if puzzle[2][col - 1] == 1 and puzzle[3][col - 1] == 1:
                    puzzle[2][col] = 0
                    puzzle[3][col] = 0
                    puzzle[2][col - 1] = 0
                    puzzle[3][col - 1] = 0
                    return True
                return False


        if col == 11:  # program the edge case
            if puzzle[2][col - 1] == 0:  # if there is not an enemy to the left, then it must be on the right, and below
                if puzzle[2][0] == 1 and puzzle[3][0] == 1:
                    puzzle[2][col] = 0
                    puzzle[3][col] = 0
                    puzzle[2][0] = 0
                    puzzle[3][0] = 0
                    return True
                return False
            if puzzle[2][0] == 0:  # if there is not an enemy to the right, then it must be on the left, and below
                if puzzle[2][col - 1] == 1 and puzzle[3][col - 1] == 1:
                    puzzle[2][col] = 0
                    puzzle[3][col] = 0
                    puzzle[2][col - 1] = 0
                    puzzle[3][col - 1] = 0
                    return True
                return False


    return False  # if the enemy is not located on 2, it is not eligible to be hammered

产生有效移动 (Generating Valid Moves)

For a given board state, I generate a list of relevant and valid moves. The procedure adheres these two philosophies: (1) I only need to spin rings with enemies on them and (2) I only need to slide the slices with enemies.

对于给定的董事会状态,我生成了相关有效动作的列表。 该过程遵循以下两种哲学:(1)我只需要旋转带有敌人的戒指,(2)我只需要滑动带有敌人的切片。

Rather than visiting each space an enemy occupies and generating moves each time, I can just check every ring or slice containing at least one enemy. This will reduce the number of moves generated by eliminating duplicate moves for enemies occupying the same ring or slice.

我可以不检查敌人占据的每个空间并每次都产生移动,而只需检查包含至少一个敌人的每个环或片。 通过消除占据相同环或片的敌人的重复动作,这将减少动作数量。

Each ring can be rotated anywhere between 1 to 11 times (inclusive). Spinning a ring 12 times results in full rotation and is therefore a useless move. Similarly, each slice can be shifted anywhere between 1 to 7 times (inclusive).

每个环可旋转1至11次(包括1次)。 将环旋转12次会导致完整旋转,因此是无用的举动。 同样,每个切片可以在1到7次(包括7次)之间移动。

# takes in matrix as argument
# returns a list of matrices containing the board state after valid moves
def generateValidMoves(puzzle):
    validMoves = []
    enemyRows = []
    enemyCols = []


    # count the number of enemies on each ring
    for row in range(0, len(puzzle)):
        if 1 in puzzle[row]:
            enemyRows.append(row)
    for row in enemyRows:
        for rotations in range(1, 12):
            copyPuzzle = copy.deepcopy(puzzle)
            actionStr = "spin ring #" + str(row) + " by " + str(rotations) + " rotations CW"
            validMoves.append([spinRing(row, rotations, copyPuzzle), actionStr])


    # count the number of enemies on each slice
    transposePuzzle = np.transpose(puzzle).tolist()
    newPuzzle = []
    # combine the columns that are directly opposite each other
    for i in range(0, 6):
        newPuzzle.append(transposePuzzle[i] + list(reversed(transposePuzzle[i + 6])))


    for col in range(0, len(newPuzzle)):
        if 1 in newPuzzle[col]:
            enemyCols.append(col)
    for col in enemyCols:
        for shifts in range(1, 8):
            copyPuzzle = copy.deepcopy(puzzle)
            actionStr = "shift slice #" + str(col) + " by " + str(shifts) + " spaces"
            validMoves.append([shiftSlice(col, shifts, copyPuzzle), actionStr])


    return validMoves

树/节点类 (Tree/Node Class)

I created a simple Node class in Python that keeps track of its data, children, parent, and depth. If you’re unfamiliar with tree data structures, I’d recommend reading about them here.

我在Python中创建了一个简单的Node类,用于跟踪其数据,子级,父级和深度。 如果您不熟悉树型数据结构,建议您在此处阅读有关它们的内容。

class Node(object):
    def __init__(self, data, parent, depth):
        self.data = data
        self.children = []
        self.parent = parent
        self.depth = depth


    def add_child(self, child):
        self.children.append(child)


    def get_data(self):
        return self.data


    def get_children(self):
        return self.children


    def get_depth(self):
        return self.depth


    def get_parent(self):
        return self.parent


    def __str__(self, level=0):
        ret = "\t" * level + repr(self.data) + "\n"
        for child in self.children:
            ret += child.__str__(level + 1)
        return ret


    def __repr__(self):
        return '<tree node representation>'

生成和遍历树 (Generating and Traversing the Tree)

Generating the tree is a recursive process that was already described in a previous section. In this section, I’ll provide the code because it contains slightly more detail.

生成树是一个递归过程,在上一节中已经描述过。 在本节中,我将提供代码,因为它包含更多细节。

# arguments: (node, matrix, integer)
# returns a tree containing all possible moves at a certain depth
def generateTree(treeNode, depth):
    sys.stdout.write("\r Generating solutions... might take a while")
    sys.stdout.flush()
    if depth == 0:
        return
    # print(treeNode.get_data())
    validMoves = generateValidMoves(treeNode.get_data()[0])
    for move in validMoves:
        childNode = Node(move, treeNode, treeNode.get_depth() + 1)
        treeNode.add_child(childNode)
        generateTree(childNode, depth - 1)
    return treeNode

After generating the tree, I traverse the tree in post order, but only collect the Nodes that are at the proper depth. Then, I verify each Node to check if it contains a winning puzzle state. If it does, I’ll collect that Node’s ancestry and store their action strings into a list.

生成树后,我按后顺序遍历树,但仅收集处于适当深度的节点。 然后,我验证每个节点以检查其是否包含获胜的拼图状态。 如果是这样,我将收集该Node的祖先并将其操作字符串存储到列表中。

# arguments: (node, list, integer)
# modifies the list containing all children at the depth
def traverse(child, childList, depth):
    for childNode in child.get_children():
        traverse(childNode, childList, depth)
    if child.get_depth() == depth:
        childList.append(child)
# arguments: list
# returns a list containing the first discovered win
def checkWinsList(childList):
    movesList = []
    for tree in childList:
        if checkWin(tree.get_data()[0]):
            while not tree.get_parent() is None:
                movesList.append(tree.get_data()[1])
                tree = tree.get_parent()
            break
    return list(reversed(movesList))

My annotations didn’t feel that helpful for this section, but it’s way easier to copy this repository and trace the logic through some debugger.

我的注释对本节没有帮助,但是通过某些调试器复制此存储库并跟踪逻辑会更容易。

使用此脚本 (Using this Script)

You can clone this GitHub repository here.

您可以在此处克隆此GitHub存储库。

After running ringsolver.py, you will be prompted with entering the total number of enemies, followed by the ring and slice of each enemy, and finally the number of ring moves. I didn’t program any validation on the user input, but the correct data type to enter is integers. You can follow the coordinate system outlined by the section ‘Matrix Representation’ for the position of each enemy.

运行ringsolver.py后 ,将提示您输入敌人总数,然后输入每个敌人的戒指和碎片 ,最后是戒指移动的数量。 我没有对用户输入进行任何验证编程,但是要输入的正确数据类型是整数。 您可以按照“矩阵表示”一节中概述的坐标系来确定每个敌人的位置。

The program outputs a list of the moves to take in order. Note that the direction the ring spins is always clockwise. For slice shifting, slices 0 to 5 shift towards the center and slices 6 to 11 shift away.

程序输出要采取的动作清单。 请注意,环旋转的方向始终是顺时针。 对于切片移位,切片0到5向中心偏移,切片6到11向远偏移。

The figure and GIF below shows an example puzzle being solved. You can verify the solution yourself!

下图和GIF显示了示例难题正在解决。 您可以自己验证解决方案!

Image for post
Image for post

As a side note, inputting 0 for the number of enemies will pull up a preloaded puzzle board to solve as a demo.

附带说明一下,输入0作为敌人的数量将拉起一个预装的拼图板以作为演示进行求解。

结束语 (Concluding Remarks)

This was surprisingly one of the more fulfilling projects I’ve worked on. It was definitely more fun designing this than it was actually solving ring puzzles. It was also a fun blog post to write and I’m really happy with how the visuals I created turned out.

令人惊讶的是,这是我从事的更令人满意的项目之一。 设计这个肯定比解决环谜题更有趣。 这也是一篇有趣的博客文章,我对我创造的视觉效果感到非常满意。

I haven’t taken many formal CS courses, so if there are any optimization techniques that anyone knows about, drop a hint in the comments and I’ll look into them! I came across some references to “pruning” while troubleshooting but I’m not entirely sure how relevant those would be for this project. I’ve only tested this algorithm for puzzles that go to depth 3, but I’m sure that puzzles at depth 4 (if they exist) would have an atrocious run time.

我没有参加过很多正式的CS课程,因此,如果有任何人知道的优化技术,请在注释中添加提示,我将进行研究! 在进行故障排除时,我遇到了一些有关“修剪”的参考,但我不完全确定这些内容与该项目的相关性。 我仅针对深度3的拼图测试了该算法,但是我敢肯定深度4(如果存在)的拼图运行时间很糟糕。

This also leads to a series of interesting questions. Does the number of rings and slices affect the required depth necessary for a puzzle? Is there a limit to the number of enemies you can have on the puzzle at once? Are there unsolvable puzzles? In any case, thanks Paper Mario for all the laughs, thoughts, and discoveries inside and outside of your game!

这也导致了一系列有趣的问题。 环和切片的数量会影响拼图所需的深度吗? 您一次可以玩拼图的敌人数量是否有限制? 有无法解决的难题吗? 无论如何,都要感谢Paper Mario在游戏内外的所有笑声,想法和发现!

翻译自: https://medium.com/python-in-plain-english/solving-paper-mario-ring-puzzles-with-tree-traversal-f34f5f3680ea

python遍历树

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值