Projetar e resolver um labirinto [em espera enquanto estiver no sandbox]

14

Sua tarefa é desempenhar o papel de ambos os personagens nesta cena desde o início. Nele, Cobb desafia Ariadne:

Você tem dois minutos para projetar um labirinto que leva um minuto para resolver.

Algumas liberdades serão tomadas nessa descrição. Mais importante ainda, esse desafio não se baseia no tempo, mas as pontuações se baseiam na eficácia de seus labirintos e solucionadores de labirinto.

Peço desculpas pelas muitas edições deste desafio, pois iteramos em direção a um formato fácil e justo.

Parte I: Formato Labirinto

Todos os labirintos são quadrados. Uma célula no labirinto é representada como uma tupla indexada a zero row column.

As paredes são representadas por duas cadeias binárias: uma para paredes horizontais (que bloqueiam o movimento entre linhas) e paredes verticais (vice-versa). Em um NxNlabirinto, existem Nx(N-1)paredes possíveis de cada tipo. Vamos dar um exemplo 3x3 em que as células são rotuladas:

A   B | C
   ---
D | E   F
   ---
G   H | I

todas as paredes verticais são possíveis: AB BC DE EF GH HI. Traduzidas em uma sequência, as paredes mostradas são 011001para paredes verticais e 010010horizontais. Além disso, com "cadeia binária", quero dizer "os caracteres '0' e '1'".

O formato completo do labirinto é uma string que contém, nesta ordem:

  • largura
  • iniciar tupla de célula
  • tupla da célula final
  • paredes horizontais
  • paredes verticais

Por exemplo, este labirinto:

   0 1 2 3 4
   _________
0 | |  E|  _|
1 |  _|_|_  |
2 |_ _ _  | |
3 |  _ _  | |
4 |____S|___|
start:(4,2)
end:(0,2)

está formatado para isso:

5
4 2
0 2
00001011101110001100
10100110000100010010

Parte II: O Arquiteto

O programa Architect cria o labirinto. Ele deve seguir as regras e fornecer um labirinto válido (aquele em que existe uma solução e o fim não está no topo do início).

Entrada: Dois números inteiros positivos:

size [random seed]

Onde sizeestará [15, 50]. Você é incentivado a usar a semente aleatória para que as correspondências possam ser repetidas, embora isso não seja necessário.

Saída: Um labirinto válido de tamanho x tamanho (quadrado) usando o formato descrito na Parte I. "válido" significa que existe uma solução e a célula inicial não é igual à célula final.

A pontuação de um arquiteto em um determinado labirinto é

   # steps taken to solve
–––––––––––––––––––––––––––––
max(dist(start,end),(# walls))

Portanto, os arquitetos são recompensados ​​por labirintos complexos, mas penalizados por cada parede construída (este é um substituto para a restrição de tempo de Ariadne). A dist()função garante que um labirinto sem paredes não obtenha uma pontuação infinita. As bordas externas do labirinto não contribuem para a contagem de paredes.

Parte III: O Solver

O Solver tenta resolver labirintos gerados por arquitetos de outros. Existe uma espécie de nevoeiro de guerra: apenas paredes adjacentes às células visitadas são incluídas (todas as outras são substituídas por '?')

input: o mesmo formato de labirinto, mas com '?' onde as paredes são desconhecidas, uma linha extra para o local atual e uma lista separada por vírgulas de opções válidas desse local. (Esta é uma grande edição que visa facilitar a gravação de uma função de análise de labirinto)

exemplo (igual ao labirinto 5x5 acima depois de dar um passo à esquerda)

5
4 2
0 2
???????????????011??
????????????????001?
4 1
4 0,4 2

O que corresponde a algo assim, onde ?está o nevoeiro:

   0 1 2 3 4
   _________
0 |????E????|
1 |?????????|
2 |?????????|
3 | ?_?_????|
4 |__C_S|_?_|

saída: uma das tuplas da lista de opções válidas

A pontuação de cada Solver é o inverso da pontuação do arquiteto.

Parte IV: Rei da Colina

Arquitetos e Solvers recebem pontuações separadas, então pode haver dois vencedores.

Cada par de arquitetos e solucionadores terá muitas chances de se superar. A pontuação será calculada sobre todos os testes e oponentes. Ao contrário do código das convenções de golfe, a pontuação média mais alta vence!

Pretendo que isso continue, mas não posso garantir testes contínuos para sempre! Digamos por enquanto que um vencedor será declarado em uma semana.

Parte V: Enviando

  • Eu mantenho poder de veto sobre todos os envios - a inteligência é incentivada, mas não se quebrar a concorrência ou o meu computador! (Se eu não souber o que seu código faz, provavelmente vou vetá-lo)
  • Crie um nome para o seu par Arquiteto / Solver. Poste seu código junto com instruções sobre como executá-lo.

Em breve: um kit de teste python atualizado para o novo formato. Ocorreram grandes mudanças para permitir envios de idiomas.

wrongu
fonte
10
Em vez de restringi-lo ao python, você não pode definir um formato de labirinto a ser criado / lido pelos competidores? Provavelmente isso interessaria mais pessoas.
Geobits 14/05
Eu tinha dois motivos para ser restritivo: o primeiro é automatizar com facilidade e segurança as partidas em execução. O segundo é evitar exigir uma biblioteca de leitura e escrita para cada idioma. Eu acho que se ninguém quer usar python eu vou ter que desistir de um ou de ambos ...
wrongu
1
Atualmente, estou escrevendo um wrapper que executa um subprograma e se comunica através de stdin / stdout. Dessa forma, você pode usar qualquer idioma que desejar. Você permitiria isso?
IchBinKeinBaum
Absolutamente! Eu estava reescrevendo todo o formato da pergunta. Devo esperar?
22614 wrongu #
1
Eu não sabia que isso era uma coisa. Eu acho que vou colocá-lo em espera por enquanto ..
wrongu 15/05

Respostas:

1

BuildFun e SolveFun

Bem, isso levou um bom tempo e não tenho certeza se o solucionador está trapaceando ou não. Embora tenha acesso a todo o labirinto o tempo todo, ele olha apenas para a célula em que está, as paredes ao redor e, se não houver uma parede entre elas, as células adjacentes a ela. Se isso for contra as regras, entre em contato e tentarei alterá-lo.

Enfim, aqui está o código:

#Architect function
def BuildFun(size,seed):
   #Initialise grid and ensure inputs are valid
   if size<15:size=15
   if size>50:size=50
   if seed<4:seed=4
   if seed>size:seed=size
   grid=[]
   for x in range(size):
      gridbuilder=[]
      for y in range(size):gridbuilder.append([0,1,1])
      grid.append(gridbuilder)
   coords=[0,0]
   grid[0][0][0]=1
   #Generate maze
   while 1:
      #Choose a preffered direction based on location in grid and seed
      pref=((((coords[0]+coords[1]+2)*int(size/2))%seed)+(seed%(abs(coords[0]-coords[1])+1)))%4
      #Find legal moves
      opt=[]
      if coords[0]>0:opt+=[0] if grid[coords[0]-1][coords[1]][0]==0 else []
      if coords[1]<size-1:opt+=[1] if grid[coords[0]][coords[1]+1][0]==0 else []
      if coords[0]<size-1:opt+=[2] if grid[coords[0]+1][coords[1]][0]==0 else []
      if coords[1]>0:opt+=[3] if grid[coords[0]][coords[1]-1][0]==0 else []
      #There are legal moves
      if len(opt)>0:
         moved=False
         while not moved:
            #Try to move in preffered direction
            if pref in opt:
               if pref==0:
                  coords[0]-=1
                  grid[coords[0]][coords[1]][0]=1
                  grid[coords[0]][coords[1]][2]=0
               elif pref==1:
                  grid[coords[0]][coords[1]][1]=0
                  coords[1]+=1
                  grid[coords[0]][coords[1]][0]=1
               elif pref==2:
                  grid[coords[0]][coords[1]][2]=0
                  coords[0]+=1
                  grid[coords[0]][coords[1]][0]=1
               else:
                  coords[1]-=1
                  grid[coords[0]][coords[1]][0]=1
                  grid[coords[0]][coords[1]][1]=0
               moved=True
            #Change preferred direction if unable to move
            else:
               pref+=1
               if pref==4:pref=0
      #There aren't legal moves
      else:
         moved=False
         #Return to a previously visited location
         if not moved:
            try:
               if grid[coords[0]-1][coords[1]][0]==1 and grid[coords[0]-1][coords[1]][2]==0:
                  grid[coords[0]][coords[1]][0]=2
                  coords[0]-=1
                  moved=True
            except:pass
         if not moved:
            try:
               if grid[coords[0]][coords[1]+1][0]==1 and grid[coords[0]][coords[1]][1]==0:
                  grid[coords[0]][coords[1]][0]=2
                  coords[1]+=1
                  moved=True
            except:pass
         if not moved:
            try:
               if grid[coords[0]+1][coords[1]][0]==1 and grid[coords[0]][coords[1]][2]==0:
                  grid[coords[0]][coords[1]][0]=2
                  coords[0]+=1
                  moved=True
            except:pass
         if not moved:
            try:
               if grid[coords[0]][coords[1]-1][0]==1 and grid[coords[0]][coords[1]-1][1]==0:
                  grid[coords[0]][coords[1]][0]=2
                  coords[1]-=1
                  moved=True
            except:pass
      #Check if finished
      fin=True
      for x in grid:
         for y in x:
            if y[0]==0:
               fin=False
               break
         if not fin:break
      if fin:break
   for x in grid:
      for y in x:
         y[0]=0
   #Find positions for start and finish such that the route between them is as long as possible
   lsf=[[0,0],[0,0],0]
   for y in range(size):
      for x in range(size):
         #Check all start positions
         lengths=[]
         coords=[[y,x,4,0]]
         while len(coords)>0:
            #Spread tracers out from start to the rest of the maze
            for coord in coords:
               opt=[]
               if coord[0]>0:opt+=[0] if grid[coord[0]-1][coord[1]][2]==0 else []
               opt+=[1] if grid[coord[0]][coord[1]][1]==0 else []
               opt+=[2] if grid[coord[0]][coord[1]][2]==0 else []
               if coord[1]>0:opt+=[3] if grid[coord[0]][coord[1]-1][1]==0 else []
               try:opt.remove(coord[2])
               except:pass
               #Dead end, tracer dies and possible end point is recorded along with length
               if len(opt)==0:
                  lengths.append([coord[0],coord[1],coord[3]])
                  coords.remove(coord)
               else:
                  #Create more tracers at branch points
                  while len(opt)>1:
                     if opt[0]==0:coords.append([coord[0]-1,coord[1],2,coord[3]+1])
                     elif opt[0]==1:coords.append([coord[0],coord[1]+1,3,coord[3]+1])
                     elif opt[0]==2:coords.append([coord[0]+1,coord[1],0,coord[3]+1])
                     else:coords.append([coord[0],coord[1]-1,1,coord[3]+1])
                     del opt[0]
                  if opt[0]==0:
                     coord[0]-=1
                     coord[2]=2
                     coord[3]+=1
                  elif opt[0]==1:
                     coord[1]+=1
                     coord[2]=3
                     coord[3]+=1
                  elif opt[0]==2:
                     coord[0]+=1
                     coord[2]=0
                     coord[3]+=1
                  else:
                     coord[1]-=1
                     coord[2]=1
                     coord[3]+=1
         #Find furthest distance and, if it's longer than the previous one, the start/end positions get updated
         lengths=sorted(lengths,key=lambda x:x[2],reverse=True)
         if lengths[0][2]>lsf[2]:lsf=[[y,x],[lengths[0][0],lengths[0][1]],lengths[0][2]]
   #Find number of walls and output maze
   w=draw(grid,size,lsf[0],lsf[1])
   #Output maze information
   print('Start = '+str(lsf[0]))
   print('End = '+str(lsf[1]))
   print('Distance = '+str(lsf[2]))
   print('Walls = '+str(w))
   print('Score = '+str(float(lsf[2])/float(w))[:5])
   #Convert array grid to binary strings horizontal and vertical
   horizontal=vertical=''
   for y in range(size):
      for x in range(size-1):vertical+=str(grid[y][x][1])
   for y in range(size-1):
      for x in range(size):horizontal+=str(grid[y][x][2])
   #Save maze information to text file for use with SolveFun
   save=open('Maze.txt','w')
   save.write(str(size)+'\n'+str(lsf[0][0])+' '+str(lsf[0][1])+'\n'+str(lsf[1][0])+' '+str(lsf[1][1])+'\n'+horizontal+'\n'+vertical)
   save.close()
#Solver function
def SolveFun():
   try:
      #Get maze information from text file
      save=open('Maze.txt','r')
      data=save.readlines()
      save.close()
      size=int(data[0])
      s=data[1].rsplit(' ')
      start=[int(s[0]),int(s[1])]
      e=data[2].rsplit(' ')
      end=[int(e[0]),int(e[1])]
      horizontal=data[3].rstrip('\n')
      vertical=data[4]
      #Build maze from information
      grid=[]
      for y in range(size):
         grid.append([])
         for x in range(size):
            grid[y].append([0,1,1])
      for y in range(size):
         for x in range(size-1):
            grid[y][x][1]=int(vertical[y*(size-1)+x])
      for y in range(size-1):
          for x in range(size):
            grid[y][x][2]=int(horizontal[y*size+x])
      path=''
      cpath=''
      bs=0
      pos=start[:]
      grid[pos[0]][pos[1]][0]=1
      while pos!=end:
         #Want to move in direction of finish
         if end[0]<pos[0] and pos[0]-end[0]>=abs(pos[1]-end[1]):pref=0
         elif end[1]>pos[1] and end[1]-pos[1]>=abs(pos[0]-end[0]):pref=1
         elif end[0]>pos[0] and end[0]-pos[0]>=abs(pos[1]-end[1]):pref=2
         else:pref=3
         #Find legal moves
         opt=[]
         if pos[0]>0:
            if grid[pos[0]-1][pos[1]][2]==0:opt+=[0]if grid[pos[0]-1][pos[1]][0]==0 else[]
         if pos[1]>0:
            if grid[pos[0]][pos[1]-1][1]==0:opt+=[3]if grid[pos[0]][pos[1]-1][0]==0 else[]
         if grid[pos[0]][pos[1]][2]==0:opt+=[2]if grid[pos[0]+1][pos[1]][0]==0 else[]
         if grid[pos[0]][pos[1]][1]==0:opt+=[1]if grid[pos[0]][pos[1]+1][0]==0 else[]
         if len(opt)>0:
            moved=False
            while not moved:
               #Try to move in preferred direction
               if pref in opt:
                  if pref==0:
                     pos[0]-=1
                     path+='0'
                     cpath+='0'
                  elif pref==1:
                     pos[1]+=1
                     path+='1'
                     cpath+='1'
                  elif pref==2:
                     pos[0]+=1
                     path+='2'
                     cpath+='2'
                  else:
                     pos[1]-=1
                     path+='3'
                     cpath+='3'
                  grid[pos[0]][pos[1]][0]=1
                  moved=True
               #Change preferred direction by 1
               else:
                  pref=(pref+1)%4
         #No legal moves, backtrack
         else:
            bs+=1
            grid[pos[0]][pos[1]][0]=2
            if int(cpath[len(cpath)-1])==0:
               pos[0]+=1
               path+='2'
            elif int(cpath[len(cpath)-1])==1:
               pos[1]-=1
               path+='3'
            elif int(cpath[len(cpath)-1])==2:
               pos[0]-=1
               path+='0'
            else:
               pos[1]+=1
               path+='1'
            cpath=cpath[:len(cpath)-1]
      #Output maze with solution as well as total steps and wasted steps
      draw(grid,size,start,end)
      print('\nPath taken:')
      print(str(len(path))+' steps')
      print(str(bs)+' backsteps')
      print(str(bs*2)+' wasted steps')
   except:print('Could not find maze')
def draw(grid,size,start,end):
   #Build output in string d
   d='   '
   for x in range(size):d+=' '+str(x)[0]
   d+='\n   '
   for x in range(size):d+='  ' if len(str(x))==1 else ' '+str(x)[1]
   d+='\n    '+'_'*(size*2-1)
   w=0
   for y in range(size):
      d+='\n'+str(y)+'  |' if len(str(y))==1 else '\n'+str(y)+' |'
      for x in range(size):
         if grid[y][x][2]:
            if start==[y,x]:d+=UL.S+'S'+UL.E
            elif end==[y,x]:d+=UL.S+'F'+UL.E
            elif grid[y][x][0]==1:d+=UL.S+'*'+UL.E
            else:d+='_'
            w+=1
         else:
            if start==[y,x]:d+='S'
            elif end==[y,x]:d+='F'
            elif grid[y][x][0]==1:d+='*'
            else:d+=' '
         if grid[y][x][1]:
            d+='|'
            w+=1
         else:d+=' '
   #Output maze and return number of walls
   print(d)
   w-=size*2
   return w
#Underlines text
class UL:
   S = '\033[4m'
   E = '\033[0m'

Percebo que isso é ridiculamente longo e não é particularmente fácil de ler, mas sou preguiçoso, e é assim que as coisas ficam.

BuildFun

O arquiteto, BuildFun, é um programa bastante simples de geração de labirinto que sempre criará um labirinto 'perfeito' (um sem loops e onde dois pontos sempre terão exatamente um caminho entre eles). Ele baseia sua lógica na entrada de sementes, o que significa que os labirintos gerados são pseudo-aleatórios com o que geralmente parece ser padrões repetidos e, com a mesma semente e tamanho, o mesmo labirinto será criado.

Uma vez que o labirinto é gerado, o programa tentará maximizar a pontuação do labirinto procurando o ponto inicial e final que resultam no caminho mais longo entre eles. Para fazer isso, ele percorre todos os pontos de partida, espalha traçadores para encontrar o ponto final mais distante e escolhe a combinação com o caminho mais longo.

Depois disso, ele desenha o labirinto, conta as paredes e exibe as informações do labirinto. Este é o ponto inicial, final, distância entre eles, número de paredes e pontuação. Ele também formata essas informações no estilo descrito acima para tamanho, início e fim, paredes horizontais e paredes verticais e as salva em um arquivo de texto chamado Maze.txt para uso posterior.

SolveFun

O solucionador, o SolveFun, usa o arquivo de texto Maze.txt como entrada e funciona de maneira muito semelhante ao arquiteto. Para cada movimento, ele escolherá a direção que deseja seguir com base em sua posição relativa até o fim e, em seguida, examinará as paredes ao seu redor. Se não houver uma parede, ela verificará se esteve na célula adjacente a ela e, se não estiver, será adicionada como opção possível. Ele se moverá na direção mais próxima da direção preferida, desde que tenha opções. Se não tiver opções, retornará até que tenha. Isso continua até chegar ao fim.

À medida que se move, registra o caminho que está seguindo no caminho variável que é usado no final para gerar o número total de etapas. Ele também registra a quantidade de vezes que teve que voltar atrás usada para calcular etapas desperdiçadas no final. Quando chegar ao fim, ele produzirá o labirinto com o caminho mais curto do início ao fim marcado com *s.

Como executar

Devido ao método de saída do labirinto (que inclui sublinhado de certos caracteres), isso deve ser executado a partir de uma linha de comando no formato

python -c 'import filename;filename.BuildFun(Size, Seed)'

e

python -c 'import filename;filename.SolveFun()'

onde Tamanho é um número inteiro entre 15 e 50 (inclusive) e Semente é um número inteiro entre 4 e Tamanho (inclusive).

Anonymous No Lifer
fonte