Ensinar você a usar o Python para implementar o jogo Sokoban (com código-fonte completo)

Introdução do projeto

Nosso projeto é um pequeno jogo Sokoban baseado em Python, chamado Sokoban:
insira a descrição da imagem aqui

O objetivo do jogo é que o jogador, também conhecido como letras maiúsculas P, empurre caixas #e preencha oburacos no chão marcados com letras minúsculas

regras do projeto

As regras para esta versão do Sokoban são as seguintes:

  • O jogo ocorre em uma grade bidimensional retangular, 原点(0,0)localizada no canto superior esquerdo
  • Cada célula na grade pode conter uma das seguintes opções a qualquer momento:
    • Pjogador representado por uma letra maiúscula
    • ' 'blocos representados por caracteres de espaço
    • #Baús representados por caracteres hash
    • *uma parede representada por um caractere asterisco
    • oBuracos representados por caracteres minúsculos
  • A cada turno, o personagem do jogador pode se mover para cima, para baixo, para a esquerda ou para a direita na grademover uma unidade
  • Os personagens dos jogadores não podem entrar em paredes ou buracos
  • A cada turno, o personagem do jogador pode empurrar a caixa uma unidade na direção em que está tentando se mover, desde que a próxima célula da caixa na direção em que o jogador está tentando se mover seja uma peça ou um buraco. Se esta condição não for atendida, nem o jogador nem o baú se moverão
  • Se o jogador empurrar a caixa para dentro do buraco, tanto o buraco quanto a caixa desaparecem, deixando apenas um tijolo.
  • Se o jogador tentar sair da borda da tela ou empurrar a caixa para fora da borda da tela, o jogador ou a caixa deve aparecer no lado oposto da tela se outras regras permitirem ,como se os dois lados da tela estivessem conectados

Documentação da interface do projeto

Neste projeto você precisa implementar a classe Sokoban com os seguintes métodos;

  • __init__(self,board): Crie uma instância de Sokoban com um determinado quadro, o quadro de parâmetros é uma lista aninhada bidimensional, que é o nosso mapa de jogo
  • find_player(self): Retorna a posição do personagem do jogador no tabuleiro. Linhas e colunas começam em 0, e a origem (0,0) está localizada no canto superior esquerdo da grade, por exemplo, a posição do jogador na introdução do nosso projeto é (0,0)
  • is_complete(self): determine se o jogo acabou. Se não houver buracos no mapa, retorne true, o que significa que o jogo acabou, caso contrário, retorne false
  • steps(self): Retorna o número de vezes que o personagem do jogador se move (isto é, quando a posição do jogador muda)
  • restart(self): Redefina a instância Sokoban para o estado anterior ao jogador iniciar o jogo
  • undo(self): Desfaz o último movimento do jogador e restaura o estado do jogo para o último movimento. Pode ser chamado repetidamente para desfazer vários movimentos.Se desfazer for chamado mais vezes do que o jogador se moveu, o tabuleiro deve permanecer em seu estado inicial
  • move(self,direction): Tenta mover o jogador uma posição e empurrar a caixa na frente do jogador. O parâmetro de direção é uma string cujos valores são w, a, s e d, representando para cima, esquerda, baixo e direita, respectivamente. Os movimentos são contados apenas quando a posição do jogador é atualizada. Se a posição do jogador não mudou, o estado do jogo não deve mudar de forma alguma
  • __str__(self): Retorna uma representação de string do mapa.Lembre-se de que as células em cada linha são separadas por espaços

Processo de realização do projeto

Escrita pré-método

Nós os implementamos um a um de acordo com as interfaces do documento. Vamos ver primeiro o método init. Só podemos pensar em duas coisas neste método:

  • inicialização do mapa
  • Inicialização das etapas do jogo de passos
    def __init__(self,board):
        self.board = board
        self.step = 0

Em seguida, implementamos o método str, que é usado aqui para percorrer a lista bidimensional. Precisamos apenas criar uma string vazia e inserir nela os elementos da lista:

    def __str__(self):
        show = ''
        num = 0
        for i in self.board:
            num += 1
            for j in i:
                show += j + ' '
            show += '\n'
        return show[:-2]

\nNós o usamos para controlar as quebras de linha depois de atravessar uma linha. Finalmente cortamos a string retornada porque: se não cortarmos, também teremos uma nova linha no final quando imprimirmos a última linha, o que é desnecessário! Podemos cortar:
insira a descrição da imagem aqui
o método is_complete(self) também usa a travessia da lista bidimensional, a ideia disso é simplesmente percorrer o mapa para ver se tem algum buraco, e a implementação é relativamente simples;

    def is_complete(self):
        for i in self.board:
            for j in i :
                if j == 'o':
                    return False
        return True

Em seguida, vamos implementar o método find_player(self). A ideia principal ainda é percorrer a lista bidimensional. Aqui, forneço dois métodos de implementação:

Método de implementação 1:

    def find_player(self):
        x = -1;y = -1
        for j in self.board:
            x += 1
            for k in j:
                y += 1
                if k == 'P':
                    return (x,y)
            y = -1

Método de implementação 2:

    def find_player(self):
        for x in range(self.board_x()):
            for y in range(self.board_x()):
                if self.board[x][y] == 'P':
                    return (x,y)

mover a escrita do método principal

Ao pensarmos na escrita do método move, à primeira vista, veremos que há muitos pontos que precisam de atenção, muitas restrições e julgamentos, e podemos nos perder ao pensar nisso. Na verdade, podemos pensar sobre isso. Toda vez que executamos o comando de movimento, na verdade, realizamos duas etapas:

  • Julgando se deve se mover
  • Mude a posição do jogador (e possivelmente do baú) se puder ser movido

Ou seja, agora decompusemos uma instrução e a tornamos um pouco mais específica. Em outras palavras, se quisermos implementar o método move, precisamos apenas concluir essas duas funções. Aqui, acho que se empilharmos essas duas funções no método move, o código ficará muito confuso e as camadas de aninhamento envolvendo if tornará o pensamento fácil de confundir. Portanto, aqui podemos abstrair a função de julgar se podemos nos mover, vamos nomeá-la como um método check. Deixe a funcionalidade de movimentação real no método move.

Por causa das quatro direções envolvidas, na verdade, depois que sabemos como realizar uma direção, a realização de outras direções é seguir a cabaça, então aqui explico apenas a direção w nos métodos de verificação e movimento e, por conveniência, pode usar A aquisição do comprimento e largura do mapa é abstraída em um método para uso posterior:

    def board_x(self):
        return len(self.board)

    def board_y(self):
        return len(self.board[0])

Em seguida, começamos a trabalhar!

Verifica()

Antes de tudo, primeiro obtemos a posição específica do jogador, basta chamar o método find_player diretamente, e sua posição pode ser entendida através do seguinte sistema de coordenadas:
insira a descrição da imagem aqui

Na direção w, seu movimento tem duas situações:

  • Caso 1: Há uma caixa adjacente a ela na direção w (ou seja, o jogador deve empurrar a caixa)
  • Caso 2: Apenas o jogador se move sozinho

E há duas situações abaixo:

  • Na direção w, a frente da caixa é uma parede

    • Neste caso, você não pode se mover
      insira a descrição da imagem aqui
  • sem parede na frente da caixa

    • Neste caso é possível mover
      insira a descrição da imagem aqui

Algumas pessoas aqui dirão que deveria haver outra situação em que há um buraco na frente da caixa. Isso mostra que não entendemos completamente o propósito de nossa abstração separada do método check.Nosso método check faz apenas uma coisa, isto é, se a caixa ou a caixa e a pessoa podem se mover. O caso em que há um buraco na frente da caixa pode ser movido, portanto não há necessidade de discuti-lo separadamente. Quanto ao cancelamento da caixa e do furo e preenchimento dos ladrilhos, não é função do nosso método de verificação, devemos colocá-los no método de movimentação para processamento.
insira a descrição da imagem aqui

Depois de considerar essas situações, não podemos começar a escrever código, porque há uma coisa que não podemos ignorar, que é o último item das regras do projeto:

  • Se o jogador tentar sair da borda da tela ou empurrar a caixa para fora da borda da tela, o jogador ou a caixa deve aparecer no lado oposto da tela se outras regras permitirem ,como se os dois lados da tela estivessem conectados

Se você for adicionar, subtrair e subtrair neste local e, em seguida, criar muitas situações, será muito problemático e propenso a problemas como marcas de canto saindo dos limites. Na verdade, podemos imaginar que, quando nosso jogador estiver avançando na direção w (supondo que não haja parede em toda a coluna que não atrapalhe o progresso), após atingir o ápice, ele aparece na parte inferior da coluna atual,Este cenário é um pouco semelhante a uma lista circular. Podemos pegá 取余的思想-lo emprestado, para que não precisemos ter discussões complicadas, e também podemos evitar erros como marcas de canto cruzando a fronteira.

Em seguida, vamos escrever o código:

    def check(self,direction):
        # 此时玩家的位置
        x = self.find_player()[0]
        y = self.find_player()[1]
        if direction not in "wasd":
            return -1
        #每个方向上的处理
        # 先考虑在你的移动方向上没有箱子的情况
        # 再考虑在你的移动方向上有箱子的情况
        # 返回正整数代表可以移动 返回1说明有箱子   返回0说明没箱子    返回负数代表不能移动
        if direction == 'w':
            # 代表方向上没有箱子
            if self.board[(x-1)%self.board_x()][y] != '#':
                if self.board[(x-1)%self.board_x()][y] not in '*o':
                    return 0
                else:
                    return -1
            # 代表方向上有箱子
            else:
                if self.board[(x-2)%self.board_x()][y] not in '*':
                    return 1
                else:
                    return -1

Aqui nosso valor de retorno;

  • Inteiro positivo significa que pode se mover
    • 0 significa que não há necessidade de empurrar a caixa, apenas o jogador se move
    • 1 significa que precisa empurrar a caixa, tanto o jogador quanto a caixa precisam se mover
  • Números negativos significam nenhum movimento

O mesmo para outras direções

jogada

Obtenha também as coordenadas do jogador primeiro e, em seguida, chame o método de verificação.Se a verificação retornar um número negativo, retorne diretamente sem processamento. Vamos nos concentrar no que acontece se o retorno for um número inteiro positivo:

Tome a direção w como um exemplo para discutir

  • Se o check retornar 0 (ou seja, apenas o jogador se move):
    • Só precisamos substituir o quadrado onde o jogador atual está com ladrilhos e substituir o quadrado anterior por P
  • Se check retornar um inteiro diferente de zero (ou seja, tanto o jogador quanto a caixa devem se mover), há dois casos aqui:
    • buraco na frente da caixa
      • A caixa e o buraco se anulam e o jogador avança
    • A frente da caixa é de ladrilhos
      • Tanto a caixa quanto o jogador avançam uma unidade

código mostra como abaixo:

    def move(self,direction):
        # 此时玩家的位置
        x = self.find_player()[0]
        y = self.find_player()[1]
        ans = self.check(direction)
        if direction == 'w':
            if ans < 0:
                return
            else:
                self.board[x][y] = ' '
                if ans == 0:
                    self.board[(x-1)%self.board_x()][y] = 'P'
                else:
                    if self.board[(x-2)%self.board_x()][y] == 'o':
                        self.board[(x-2)%self.board_x()][y] = ' '
                        self.board[(x-1)%self.board_x()][y] = 'P'
                    else:
                        self.board[(x-2)%self.board_x()][y] = '#'
                        self.board[(x-1)%self.board_x()][y] = 'P'

Encerramento do projeto

A partir de agora, ainda temos os seguintes três métodos que não foram implementados:

  • steps(self)
  • restart(self)
  • undo(self)

O método de passos registra os passos de movimento do jogador. Em nosso projeto atual, todas as operações de movimento estão relacionadas ao método de movimento. Podemos contar diretamente os passos no método de movimento:
insira a descrição da imagem aqui

    def steps(self):
        return self.step

Observe que contamos apenas se nosso método de verificação retornar um número não negativo.

A seguir, veremos os métodos restart(self) e undo(self).Um desses dois métodos é usado para recomeçar e o outro é usado para reverter. Todos eles possuem uma característica que é o retrocesso do estado. Então podemos tratá-los com o mesmo pensamento, pois a diferença entre eles nada mais é do que um estado de voltar ao início e um estado de voltar ao passo anterior.

O método específico é: enquanto a posição (estado) do jogador mudar, armazenaremos o estado do mapa antes da mudança. A nível de código, é armazenar a placa antes da alteração em uma lista especial. Aqui damos o nome de histórico desta lista:
insira a descrição da imagem aqui
insira a descrição da imagem aqui
Tenha muito cuidado aqui, não é possível armazená-la na lista como a seguir:

self.history.append(self.board)

Eventualmente, você descobrirá que todos os elementos armazenados no histórico são os mesmos e consistentes com o quadro atual. Isso ocorre porque a lista é um tipo de dados mutável em Python e seu valor de endereço não será alterado, mesmo que seja alterado. Usando o método acima, cada elemento que armazenamos no histórico tem o mesmo valor de endereço, ou seja, são o mesmo objeto. Portanto, para evitar esse tipo de problema aqui, devemos usar cópia profunda, e cópia profunda comum é inútil para nossa lista aninhada multidimensional.

Aqui eu tentei vários dos métodos mais comuns na Internet sem sucesso:

  • O método copy da lista
  • método list()
  • [:]método da fatia

Podemos usar o método deepcopy na cópia do módulo interno do Python, o código é o seguinte:

import copy

···
self.history.append(copy.deepcopy(self.board))

A seguir, só precisamos retirar diferentes estados do histórico no método correspondente:

    def restart(self):
        self.board = self.history[0]
        self.step = 0
        self.history.clear();


    def undo(self):
        self.board = self.history[-1]
        self.step -= 1
        self.history.pop()

Perceber:

  • Depois de reiniciar, precisamos limpar a lista de histórico
  • Ao reverter, além de retornar o último elemento do histórico, também precisamos excluí-lo da lista, caso contrário, ocorrerá um erro ao reverter duas ou mais vezes

projeto perfeito

Na verdade, negligenciamos alguns pontos quando escrevemos o código:

  • Se desfazer for chamado mais vezes do que o jogador se moveu, o tabuleiro deve permanecer em seu estado inicial

Ou seja, nem sempre podemos reverter, de acordo com nosso código, se continuarmos revertendo, ocorrerão as duas situações a seguir:

  • O problema do comprimento da lista de histórico levará a erros na marca de canto
  • Nosso passo se tornará negativo

Melhorar:

    def undo(self):
        if self.step == 0:
            return
        self.board = self.history[-1]
        self.step -= 1
        self.history.pop()
  • Se reiniciarmos no início ou reiniciarmos várias vezes seguidas, um erro será relatado

A essência também é que o erro de marca de canto é causado pelo fato de a lista de histórico estar vazia

Melhorar:

    def restart(self):
        if len(self.history) == 0:
            return
        self.board = self.history[0]
        self.step = 0
        self.history.clear();

O código-fonte geral do projeto

'''
 推箱子
 P代表玩家 o代表洞  #代表箱子 空字符代表地砖
 项目要求:
 1)二维网格的元原点位于左上方
 2)每回合只能上下左右移动一格
 3)玩家不能移动到墙或者洞中
 4)只有当玩家推动箱子移动的下一个单位是地砖或着洞的时候才能移动成功  否则箱子不会移动
 5)箱子进入洞中之后  洞和箱子都会消失   使用地砖进行替代
 6)如果离开屏幕边缘 在规则允许的情况下(也就是第4条)  允许出现在对侧
'''


import copy

class Sokoban:
    def __init__(self,board):
        self.board = board
        self.step = 0
        self.history = []
    def __str__(self):
        show = ''
        num = 0
        for i in self.board:
            num += 1
            for j in i:
                show += j + ' '
            show += '\n'
        return show[:-2]
    def find_player(self):
        for x in range(self.board_x()):
            for y in range(self.board_x()):
                if self.board[x][y] == 'P':
                    return (x,y)

    def is_complete(self):
        for i in self.board:
            for j in i :
                if j == 'o':
                    return False
        return True
    def steps(self):
        return self.step

    def restart(self):
        if len(self.history) == 0:
            return
        self.board = self.history[0]
        self.step = 0
        self.history.clear();


    def undo(self):
        if self.step == 0:
            return
        self.board = self.history[-1]
        self.step -= 1
        self.history.pop()

    def move(self,direction):
        # 此时玩家的位置
        x = self.find_player()[0]
        y = self.find_player()[1]
        ans = self.check(direction)
        if direction == 'w':
            if ans < 0:
                return
            else:
                self.step += 1
                self.history.append(copy.deepcopy(self.board))
                self.board[x][y] = ' '
                if ans == 0:
                    self.board[(x-1)%self.board_x()][y] = 'P'
                else:
                    if self.board[(x-2)%self.board_x()][y] == 'o':
                        self.board[(x-2)%self.board_x()][y] = ' '
                        self.board[(x-1)%self.board_x()][y] = 'P'
                    else:
                        self.board[(x-2)%self.board_x()][y] = '#'
                        self.board[(x-1)%self.board_x()][y] = 'P'
        elif direction == 'a':
            if ans < 0:
                return
            else:
                self.step += 1
                self.history.append(copy.deepcopy(self.board))

                self.board[x][y] = ' '

                if ans == 0:
                    self.board[x][(y - 1)%self.board_y()] = 'P'
                else:
                    if self.board[x][(y - 2)%self.board_y()] == 'o':
                        self.board[x][(y - 2)%self.board_y()] = ' '
                        self.board[x][(y - 1)%self.board_y()] = 'P'
                    else:
                        self.board[x][(y - 2)%self.board_y()] = '#'
                        self.board[x][(y - 1)%self.board_y()] = 'P'
        elif direction == 's':
            if ans < 0:
                return
            else:
                self.step += 1
                self.history.append(copy.deepcopy(self.board))

                self.board[x][y] = ' '
                if ans == 0:
                    self.board[(x + 1)%self.board_x()][y] = 'P'
                else:
                    if self.board[(x + 2)%self.board_x()][y] == 'o':
                        self.board[(x + 2)%self.board_x()][y] = ' '
                        self.board[(x + 1)%self.board_x()][y] = 'P'
                    else:
                        self.board[(x + 2)%self.board_x()][y] = '#'
                        self.board[(x + 1)%self.board_x()][y] = 'P'
        elif direction == 'd':
            if ans < 0:
                return
            else:
                self.step += 1
                self.history.append(copy.deepcopy(self.board))

                self.board[x][y] = ' '
                if ans == 0:
                    self.board[x][(y + 1)%self.board_y()] = 'P'
                else:
                    if self.board[x][(y + 2)%self.board_y()] == 'o':
                        self.board[x][(y + 2)%self.board_y()] = ' '
                        self.board[x][(y + 1)%self.board_y()] = 'P'
                    else:
                        self.board[x][(y + 2)%self.board_y()] = '#'
                        self.board[x][(y + 1)%self.board_y()] = 'P'

    def check(self,direction):
        # 此时玩家的位置
        x = self.find_player()[0]
        y = self.find_player()[1]
        if direction not in "wasd":
            return -1
        #每个方向上的处理
        # 先考虑在你的移动方向上没有箱子的情况
        # 再考虑在你的移动方向上有箱子的情况
        # 返回正整数代表可以移动 返回1说明有箱子   返回0说明没箱子    返回负数代表不能移动
        if direction == 'w':
            # 代表方向上没有箱子
            if self.board[(x-1)%self.board_x()][y] != '#':
                if self.board[(x-1)%self.board_x()][y] not in '*o':
                    return 0
                else:
                    return -1
            # 代表方向上有箱子
            else:
                if self.board[(x-2)%self.board_x()][y] not in '*':
                    return 1
                else:
                    return -1
        elif direction == 'a':
            # 代表方向上没有箱子
            if self.board[x][(y - 1)%self.board_y()] != '#':
                if self.board[x][(y - 1)%self.board_y()] not in '*o':
                    return 0
                else:
                    return -1
            # 代表方向上有箱子
            else:
                if self.board[x][(y - 2)%self.board_y()] not in '*':
                    return 1
                else:
                    return -1
        elif direction == 's':
            # 代表方向上没有箱子
            if self.board[(x + 1)%self.board_x()][y] != '#':
                if self.board[(x + 1)%self.board_x()][y] not in '*o':
                    return 0
                else:
                    return -1
            # 代表方向上有箱子
            else:
                if self.board[(x + 2)%self.board_x()][y] not in '*':
                    return 1
                else:
                    return -1
        elif direction == 'd':
            # 代表方向上没有箱子
            if self.board[x][(y + 1)%self.board_y()] != '#':
                if self.board[x][(y + 1)%self.board_y()] not in '*o':
                    return 0
                else:
                    return -1
            # 代表方向上有箱子
            else:
                if self.board[x][(y + 2)%self.board_y()] not in '*':
                    return 1
                else:
                    return -1

    def board_x(self):
        return len(self.board)

    def board_y(self):
        return len(self.board[0])

# 竖着是x轴  横着是y轴
board = [
    ['*', '*', ' ', '*', '*'],
    ['*', 'o', ' ', ' ', '*'],
    ['#', ' ', 'P', '#', 'o'],
    ['*', ' ', ' ', ' ', '*'],
    ['*', '*', ' ', '*', '*'],
]


game = Sokoban(board)
move = str()
print(game)
print(game.steps(), ':', game.is_complete())
while not game.is_complete():
    move = input('move:')
    if move == 'u':
        game.undo()
    elif move == 'r':
        game.restart()
    else:
        game.move(move)
    print(game)
    print(game.steps(), ':', game.is_complete())

resultado em execução;
insira a descrição da imagem aqui

O cv direto pode ser reproduzido no IDE, e o mapa pode ser customizado por você, lembre-se que o número de caixas e buracos deve ser o mesmo, caso contrário você nunca passará de nível.

Análise de defeitos do projeto

  • A estrutura de dados utilizada no projeto é relativamente simples, e a lista é utilizada até o final. Na verdade, em muitos lugares, estruturas de dados como pilhas, filas e listas encadeadas podem ser usadas para otimização relacionada.
  • O código em alguns lugares é um pouco redundante e pode ser otimizado sintaticamente
  • Por causa dos grilhões do documento de interface, existem muitas maneiras de refiná-lo. Por exemplo, a lógica do código do método move ou do método check ainda é um pouco demais. Existe alguma lógica que os dois métodos podem compartilhar, e podemos abstraí-la e criar um novo método.
  • Como a lógica nas quatro direções de wasd é semelhante, podemos considerar a abstração secundária em vez de colocar todos os quatro casos nos métodos check e move.

Projeto colheita e reflexão

Este pequeno jogo de Sokoban é um pequeno dever de casa atribuído pelo professor na escola. Como costumo usar algumas coisas de front-end e java back-end, não preciso esquecer o python por muito tempo. Quando eu estava escrevendo, a sintaxe orientada a objetos e alguns métodos relacionados a listas foram escritos enquanto verificava a documentação. Isso faz com que o código não seja muito maduro e robusto.

Claro, ganhei muito, um é revisar o python e depoisA ideia de pegar o restante. Posso encontrá-lo quando costumo fazer perguntas de algoritmo, mas não o usei muito durante o desenvolvimento. Este projeto me permitiu ver o papel da subtração no desenvolvimento real: pode reduzir erros enquanto otimiza o código. Quando eu não esperava pegar o restante no começo, a discussão confidencial de cada situação era simplesmente enlouquecedora.

Existe outro ponto muito importante, que é a necessidade de esboçar antes de escrever o código

Na verdade, seja desenvolvimento front-end ou desenvolvimento back-end, geralmente pensamos mais sobre: ​​o que usar e como usar. Esse tipo de pensamento lógico estrito na verdade não é muito frequente. Como resultado, um computador e um documento podem basicamente resolver o problema. Quando se trata de algoritmos ou classificação e consideração estritas de situação, precisamos escrever um rascunho para organizar nossas ideias antes de escrever o código. Escrever código diretamente ou não fazer um rascunho afetará muito a eficiência e a qualidade.

Acho que você gosta

Origin blog.csdn.net/zyb18507175502/article/details/127817500
Recomendado
Clasificación