Coniglietto saltellante di Google


16

Il 4 dicembre 2017, Google Doodle è stato un gioco di programmazione grafico con un coniglio . I livelli successivi erano piacevolmente non banali e sembravano un ottimo candidato per una sfida di .

Dettagli

Gioco

  • Sono disponibili quattro mosse: salta in avanti, gira a sinistra, gira a destra e continua. Ognuna di queste mosse è un gettone , corrispondente al fatto che sono ciascuna tessera nel gioco.
  • Il coniglietto può affrontare quattro direzioni ortogonali (ovvero nord, sud, est, ovest).
  • Il coniglietto può saltare in avanti (spostare di un quadrato nella direzione in cui è rivolto) e girare a sinistra o a destra.
  • I loop possono contenere qualsiasi numero di altre mosse al loro interno, inclusi altri loop, e il loro conteggio delle iterazioni è un numero intero positivo (sebbene il gioco tecnicamente consenta un conteggio delle iterazioni pari a 0).
  • Il tabellone è un insieme di quadrati allineati a griglia e il coniglietto può saltare tra i quadrati adiacenti.
  • Il coniglietto non può saltare nel vuoto. Ciò significa che un tentativo di saltare fuori dal tabellone non fa nulla. (Questa apparentemente è stata una sorpresa per alcune persone e una delusione per altri.)
  • I quadrati sono contrassegnati o non contrassegnati. Quando il coniglietto si trova su un quadrato, diventa segnato.
  • Il livello è completo quando tutti i quadrati sono contrassegnati.
  • Si può presumere che esista una soluzione.

Il tuo codice

  • Obiettivo: data una scheda, trovare una o più soluzioni più brevi.
  • L'input è un elenco di posizioni quadrate che formano la scheda (distinguendo i quadrati contrassegnati e non contrassegnati) e l'output è un elenco di mosse. Il formato di input e output non ha alcuna importanza, a condizione che siano leggibili e comprensibili.
  • Criterio vincente: somma del numero di mosse delle soluzioni più brevi trovate entro un minuto per ogni tavola. Se il tuo programma non trova una soluzione per una particolare scheda, il tuo punteggio per quella scheda è (5 * numero di quadrati).
  • Si prega di non codificare le soluzioni in alcun modo. Il tuo codice dovrebbe essere in grado di prendere qualsiasi scheda come input, non solo quelle fornite come esempi di seguito.

Esempi

Le soluzioni sono nascoste negli spoiler per darti la possibilità di giocare prima e provare alcuni di questi da soli. Inoltre, viene fornita una sola soluzione di seguito per ciascuna.

Sè la piazza iniziale del coniglietto (rivolta verso est), #è una piazza non contrassegnata ed Oè una piazza segnata. Per le mosse, la mia notazione è F= salta in avanti, L= gira a sinistra, R= gira a destra e LOOP(<num>){<moves>}indica un ciclo che scorre <num>ripetutamente e lo fa <moves>ogni volta. Se il ciclo può essere eseguito un numero qualsiasi di volte oltre un numero minimo, <num>può essere omesso (vale a dire l'infinito funziona).

Livello 1:

S##

FF

Livello 2:

S##
  #
  #

LOOP (2) {} FFR

Livello 3:

S##
# #
###

LOOP {} FFR

Livello 4:

###
# #
##S##
  # #
  ###

LOOP {F LOOP (7) {FL}} (trovato da DJMcMayhem)

Livello 5:

#####
# # #
##S##
# # #
#####

LOOP (18) {LOOP (10) {FR} L}
Fonte: Reddit

Livello 6:

 ###
#OOO#
#OSO#
#OOO#
 ###

LOOP {LOOP (3) {F} L}

Enormi schede: (soluzioni più brevi attualmente sconosciute)

12x12:

S###########
############
############
############
############
############
############
############
############
############
############
############

Livello 5 ma molto più grande:

#############
# # # # # # #
#############
# # # # # # #
#############
# # # # # # #
######S######
# # # # # # #
#############
# # # # # # #
#############
# # # # # # #
#############

Più buche:

S##########
###########
## ## ## ##
###########
###########
## ## ## ##
###########
###########
## ## ## ##
###########
###########

e

S#########
##########
##  ##  ##
##  ##  ##
##########
##########
##  ##  ##
##  ##  ##
##########
##########

Infine, l'asimmetria può essere un vero dolore nel culo:

#######
# ##  #
#######
###S###
# ##  #
# ##  #
#######

e

#########
# ##  ###
###S  ###
# #######
###    ##
#####   #
####  ###
#########
#########


"trova una o più soluzioni più brevi" Pensavo che l'arresto del problema lo proibisse
Leaky Nun

@Leaky Nun Questo non è correlato al problema di arresto. Questa è una ricerca per grafici
WhatToDo il

Ma il looping è consentito ...
Leaky Nun,

4
Penso che non si applichi perché il consiglio è finito. Per ogni ciclo, funziona per sempre o si ferma. Un loop senza loop al suo interno eseguirà il loop per sempre solo se l'argomento per il numero di iterazioni viene eliminato. In tal caso, il numero finito di stati della scheda garantisce che il ciclo inizierà a ripetere stati, che possono essere verificati.
WhatToDo il

Risposte:


12

Python 3, 67 token

import sys
import time

class Bunny():
    def __init__(self):
        self.direction = [0, 1]
        self.coords = [-1, -1]

    def setCoords(self, x, y):
        self.coords = [x, y]

    def rotate(self, dir):
        directions = [[1, 0], [0, 1], [-1, 0], [0, -1]]
        if dir == 'L':
            self.direction = directions[(directions.index(self.direction) + 1) % 4]
        if dir == 'R':
            self.direction = directions[(directions.index(self.direction) - 1) % 4]

    def hop(self):
        self.coords = self.nextTile()

    # Returns where the bunny is about to jump to
    def nextTile(self):
        return [self.coords[0] + self.direction[0], self.coords[1] + self.direction[1]]

class BoardState():
    def __init__(self, map):
        self.unvisited = 0
        self.map = []

        self.bunny = Bunny()
        self.hopsLeft = 0

        for x, row in enumerate(map):
            newRow = []
            for y, char in enumerate(row):
                if char == '#':
                    newRow.append(1)
                    self.unvisited += 1

                elif char == 'S':
                    newRow.append(2)

                    if -1 in self.bunny.coords:
                        self.bunny.setCoords(x, y)
                    else:
                        print("Multiple starting points found", file=sys.stderr)
                        sys.exit(1)

                elif char == ' ':
                    newRow.append(0)

                elif char == 'O':
                    newRow.append(2)

                else:
                    print("Invalid char in input", file=sys.stderr)
                    sys.exit(1)

            self.map.append(newRow)

        if -1 in self.bunny.coords:
            print("No starting point defined", file=sys.stderr)
            sys.exit(1)

    def finished(self):
        return self.unvisited == 0

    def validCoords(self, x, y):
        return -1 < x < len(self.map) and -1 < y < len(self.map[0])

    def runCom(self, com):
        if self.finished():
            return

        if self.hopsLeft < self.unvisited:
            return

        if com == 'F':
            x, y = self.bunny.nextTile()
            if self.validCoords(x, y) and self.map[x][y] != 0:
                self.bunny.hop()
                self.hopsLeft -= 1

                if (self.map[x][y] == 1):
                    self.unvisited -= 1
                self.map[x][y] = 2

        else:
            self.bunny.rotate(com)

class loop():
    def __init__(self, loops, commands):
        self.loops = loops
        self.commands = [*commands]

    def __str__(self):
        return "loop({}, {})".format(self.loops, list(self.commands))

    def __repr__(self):
        return str(self)


def rejectRedundantCode(code):
    if isSnippetRedundant(code):
        return False

    if type(code[-1]) is str:
        if code[-1] in "LR":
            return False
    else:
        if len(code[-1].commands) == 1:
            print(code)
            if code[-1].commands[-1] in "LR":
                return False

    return True


def isSnippetRedundant(code):
    joined = "".join(str(com) for com in code)

    if any(redCode in joined for redCode in ["FFF", "RL", "LR", "RRR", "LLL"]):
        return True

    for com in code:
        if type(com) is not str:
            if len(com.commands) == 1:
                if com.loops == 2:
                    return True

                if type(com.commands[0]) is not str:
                    return True

                if com.commands[0] in "LR":
                    return True

            if len(com.commands) > 1 and len(set(com.commands)) == 1:
                return True

            if isSnippetRedundant(com.commands):
                return True

    for i in range(len(code)):
        if type(code[i]) is not str and len(code[i].commands) == 1:
            if i > 0 and code[i].commands[0] == code[i-1]:
                return True
            if i < len(code) - 1 and code[i].commands[0] == code[i+1]:
                return True

        if type(code[i]) is not str:
            if i > 0 and type(code[i-1]) is not str and code[i].commands == code[i-1].commands:
                return True
            if i < len(code) - 1 and type(code[i+1]) is not str and code[i].commands == code[i+1].commands:
                return True

            if len(code[i].commands) > 3 and all(type(com) is str for com in code[i].commands):
                return True

    return False

def flatten(code):
    flat = ""
    for com in code:
        if type(com) is str:
            flat += com
        else:
            flat += flatten(com.commands) * com.loops

    return flat

def newGen(n, topLevel = True):
    maxLoops = 9
    minLoops = 2
    if n < 1:
        yield []

    if n == 1:
        yield from [["F"], ["L"], ["R"]]

    elif n == 2:
        yield from [["F", "F"], ["F", "L"], ["F", "R"], ["L", "F"], ["R", "F"]]

    elif n == 3:
        for innerCode in newGen(n - 1, False):
            for loops in range(minLoops, maxLoops):
                if len(innerCode) != 1 and 0 < innerCode.count('F') < 2:
                    yield [loop(loops, innerCode)]

        for com in "FLR":
            for suffix in newGen(n - 2, False):
                for loops in range(minLoops, maxLoops):
                    if com not in suffix:
                        yield [loop(loops, [com])] + suffix

    else:
        for innerCode in newGen(n - 1, False):
            if topLevel:
                yield [loop(17, innerCode)]
            else:
                for loops in range(minLoops, maxLoops):
                    if len(innerCode) > 1:
                        yield [loop(loops, innerCode)]

        for com in "FLR":
            for innerCode in newGen(n - 2, False):
                for loops in range(minLoops, maxLoops):
                    yield [loop(loops, innerCode)] + [com]
                    yield [com] + [loop(loops, innerCode)]

def codeLen(code):
    l = 0
    for com in code:
        l += 1
        if type(com) is not str:
            l += codeLen(com.commands)

    return l


def test(code, board):
    state = BoardState(board)
    state.hopsLeft = flatten(code).count('F')

    for com in code:
        state.runCom(com)


    return state.finished()

def testAll():
    score = 0
    for i, board in enumerate(boards):
        print("\n\nTesting board {}:".format(i + 1))
        #print('\n'.join(board),'\n')
        start = time.time()

        found = False
        tested = set()

        for maxLen in range(1, 12):
            lenCount = 0
            for code in filter(rejectRedundantCode, newGen(maxLen)):
                testCode = flatten(code)
                if testCode in tested:
                    continue

                tested.add(testCode)

                lenCount += 1
                if test(testCode, board):
                    found = True

                    stop = time.time()
                    print("{} token solution found in {} seconds".format(maxLen, stop - start))
                    print(code)
                    score += maxLen
                    break

            if found:
                break

    print("Final Score: {}".format(score))

def testOne(board):
    start = time.time()
    found = False
    tested = set()
    dupes = 0

    for maxLen in range(1, 12):
        lenCount = 0
        for code in filter(rejectRedundantCode, newGen(maxLen)):
            testCode = flatten(code)
            if testCode in tested:
                dupes += 1
                continue

            tested.add(testCode)

            lenCount += 1
            if test(testCode, board):
                found = True
                print(code)
                print("{} dupes found".format(dupes))
                break

        if found:
            break

        print("Length:\t{}\t\tCombinations:\t{}".format(maxLen, lenCount))

    stop = time.time()
    print(stop - start)

#testAll()
testOne(input().split('\n'))

Questo programma testerà una singola scheda di input, ma trovo questo driver di test più utile . Testerà ogni singola scheda allo stesso tempo e stamperà quanto tempo ci è voluto per trovare quella soluzione. Quando eseguo quel codice sulla mia macchina (CPU quad core Intel i7-7700K a 4,20 GHz, 16,0 GB RAM), ottengo il seguente output:

Testing board 1:
2 token solution found in 0.0 seconds
['F', 'F']


Testing board 2:
4 token solution found in 0.0025103092193603516 seconds
[loop(17, [loop(3, ['F']), 'R'])]


Testing board 3:
4 token solution found in 0.0010025501251220703 seconds
[loop(17, [loop(3, ['F']), 'L'])]


Testing board 4:
5 token solution found in 0.012532949447631836 seconds
[loop(17, ['F', loop(7, ['F', 'L'])])]


Testing board 5:
5 token solution found in 0.011022329330444336 seconds
[loop(17, ['F', loop(5, ['F', 'L'])])]


Testing board 6:
4 token solution found in 0.0015044212341308594 seconds
[loop(17, [loop(3, ['F']), 'L'])]


Testing board 7:
8 token solution found in 29.32585096359253 seconds
[loop(17, [loop(4, [loop(5, [loop(6, ['F']), 'L']), 'L']), 'F'])]


Testing board 8:
8 token solution found in 17.202533721923828 seconds
[loop(17, ['F', loop(7, [loop(5, [loop(4, ['F']), 'L']), 'F'])])]


Testing board 9:
6 token solution found in 0.10585856437683105 seconds
[loop(17, [loop(7, [loop(4, ['F']), 'L']), 'F'])]


Testing board 10:
6 token solution found in 0.12129759788513184 seconds
[loop(17, [loop(7, [loop(5, ['F']), 'L']), 'F'])]


Testing board 11:
7 token solution found in 4.331984758377075 seconds
[loop(17, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]


Testing board 12:
8 token solution found in 58.620323181152344 seconds
[loop(17, [loop(3, ['F', loop(4, [loop(3, ['F']), 'R'])]), 'L'])]

Final Score: 67

Quest'ultimo test si limita a malapena a sotto la limitazione del minuto.

sfondo

Questa è stata una delle sfide più divertenti a cui abbia mai risposto! Ho avuto uno schema esplosivo a caccia e alla ricerca di euristiche per ridurre le cose.

In genere, qui su PPCG tendo a rispondere a domande relativamente semplici. Sono particolarmente affezionato al tag perché generalmente è abbastanza adatto per le mie lingue. Un giorno, circa due settimane fa, stavo guardando attraverso i miei badge e mi sono reso conto che non avevo mai ottenuto il badge di risveglio . Così ho guardato senza rispostaper vedere se qualcosa ha attirato la mia attenzione e ho trovato questa domanda. Ho deciso di rispondere a prescindere dal costo. Alla fine è stato un po 'più difficile di quanto pensassi, ma alla fine ho avuto una risposta di forza bruta di cui posso dire di essere orgoglioso. Ma questa sfida è totalmente fuori dalla norma per me dal momento che di solito non passo più di un'ora per una singola risposta. Questa risposta mi ha richiesto un po 'più di 2 settimane e almeno 10+ di lavoro per arrivare finalmente a questo livello, anche se non stavo tenendo traccia con attenzione.

La prima iterazione fu una soluzione di pura forza bruta. Ho usato il seguente codice per generare tutti gli snippet fino alla lunghezza N :

def generateCodeLenN(n, maxLoopComs, maxLoops, allowRedundant = False):
    if n < 1:
        return []

    if n == 1:
        return [["F"], ["L"], ["R"]]

    results = []

    if 1:
        for com in "FLR":
            for suffix in generateCodeLenN(n - 1, maxLoopComs, maxLoops, allowRedundant):
                if allowRedundant or not isSnippetRedundant([com] + suffix):
                    results.append([com] + suffix)

    for loopCount in range(2, maxLoopComs):
        for loopComs in range(1, n):
            for innerCode in generateCodeLenN(loopComs, maxLoopComs, maxLoops - 1, allowRedundant):
                if not allowRedundant and isSnippetRedundant([loop(loopCount, innerCode)]):
                    continue

                for suffix in generateCodeLenN(n - loopComs - 1, maxLoopComs, maxLoops - 1, allowRedundant):
                    if not allowRedundant and isSnippetRedundant([loop(loopCount, innerCode)] + suffix):
                        continue

                    results.append([loop(loopCount, innerCode)] + suffix)

                if loopComs == n - 1:
                    results.append([loop(loopCount, innerCode)])

    return results

A questo punto, ero sicuro che testare ogni singola risposta possibile sarebbe stato troppo lento, quindi ero solito isSnippetRedundantfiltrare i frammenti che potevano essere scritti con un frammento più breve. Ad esempio, rifiuterei di produrre lo snippet ["F", "F", "F"]perché si potrebbero ottenere esattamente gli stessi effetti [Loop(3, ["F"]), quindi se arriviamo al punto in cui testiamo i frammenti di lunghezza 3, sappiamo che nessun frammento di lunghezza 3 potrebbe risolvere la scheda corrente. Questo ha usato molti buoni mnemonici, ma alla fine lo è stato waaaaytroppo lento. Testcase 12 ha impiegato poco più di 3.000 secondi usando questo approccio. Questo è chiaramente significativamente troppo lento. Ma usando queste informazioni e un mucchio di cicli di computer per forzare soluzioni brevi su ogni scheda, ho potuto trovare un nuovo modello. Ho notato che quasi ogni soluzione trovata sembrerebbe in genere simile a quanto segue:

[<com> loop(n, []) <com>]

nidificato diversi livelli in profondità, con le singole virgole su ciascun lato facoltative. Ciò significa che soluzioni come:

["F", "F", "R", "F", "F", "L", "R", "F", "L"]

non apparirebbe mai. In effetti, non vi è mai stata una sequenza di più di 3 token non loop. Un modo per utilizzare questo sarebbe filtrare tutti questi e non preoccuparsi di testarli. Ma generarli stava ancora impiegando una quantità non trascurabile di tempo e filtrare attraverso i milioni di frammenti come questo avrebbe a malapena tagliato il tempo libero. Invece ho riscritto drasticamente il generatore di codice per generare solo frammenti seguendo questo schema. In pseudo codice, il nuovo generatore segue questo schema generale:

def codeGen(n):
    if n == 1:
        yield each [<com>]

    if n == 2:
        yield each [<com>, <com>]

    if n == 3:
        yield each [loop(n, <com length 2>]
        yield each [loop(n, <com>), <com>]

    else:
        yield each [loop(n, <com length n-1>)]
        yield each [loop(n, <com length n-2>), <com>]
        yield each [<com>, loop(n, <com length n-2>)]

        # Removed later
        # yield each [<com>, loop(n, <com length n-3>), <com>]
        # yield each [<com>, <com>, loop(n, <com length n-3>)]
        # yield each [loop(n, <com length n-3>), <com>, <com>]

Ciò ha ridotto il test case più lungo a 140 secondi, il che è un ridicolo miglioramento. Ma da qui, c'erano ancora alcune cose che dovevo migliorare. Ho iniziato a filtrare in modo più aggressivo il codice ridondante / inutile e verificando se il codice è stato testato prima. Questo ha ridotto ulteriormente, ma non era abbastanza. Alla fine, l'ultimo pezzo che mancava era il contatore loop. Attraverso il mio algoritmo altamente avanzato (leggi: prova ed errore casuali ) ho determinato che l'intervallo ottimale per consentire l'esecuzione dei loop è [3-8]. Ma c'è un enorme miglioramento: se sappiamo che [loop(8, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]non è in grado di risolvere il nostro consiglio, allora non c'è assolutamente modo[loop(3, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]o qualsiasi conteggio dei cicli da 3-7 potrebbe risolverlo. Quindi, anziché scorrere tutte le dimensioni del loop da 3-8, impostiamo il conteggio del loop sul loop esterno al massimo. Questo finisce per ridurre lo spazio di ricerca di un fattore di maxLoop - minLoop, o 6 in questo caso.

Ciò ha aiutato molto, ma alla fine ha gonfiato il punteggio. Alcune soluzioni che avevo trovato in precedenza con la forza bruta richiedono l'esecuzione di numeri di loop maggiori (ad esempio, le schede 4 e 6). Quindi, anziché impostare il conteggio del loop esterno su 8, impostiamo il conteggio del loop esterno su 17, un numero magico calcolato anche dal mio algoritmo altamente avanzato. Sappiamo di poterlo fare perché aumentare il numero di loop del loop più esterno non ha alcun effetto sulla validità della soluzione. Questo passaggio in realtà ha ridotto il nostro punteggio finale di 13. Quindi non è un passaggio banale.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.