GoogleのHopping Bunny


16

2017年12月4日、Google Doodleはバニーをフィーチャーしたグラフィカルプログラミングゲームでした。後のレベルは非常に重要であり、挑戦の素晴らしい候補のように見えました。

詳細

ゲーム

  • 次の4つの動きがあります:前方にホップ、左折、右折、ループ。これらの動きのそれぞれが1つのトークンであり、それぞれがゲーム内の1つのタイルであることに対応しています。
  • バニーは直交する4つの方向(北、南、東、西)に直面できます。
  • バニーは前に飛び出して(正面を向く方向に1マス移動)、左または右に曲がります。
  • ループには、他のループを含む他の動きがいくつもあり、それらの反復カウントは正の整数です(ただし、ゲームでは技術的に反復カウント0を許可しています)。
  • ボードはグリッドに合わせた正方形のセットで、バニーは隣接する正方形の間を飛び回ることができます。
  • バニーはボイドに飛び込むことができません。ボードから飛び降りようとしても何もしないという意味です。(これは明らかに一部の人々にとっては驚きであり、他の人々にとっては失望でした。)
  • 四角はマーク付きまたはマークなしのいずれかです。バニーが正方形の上にあるとき、それはマークされます。
  • すべての正方形がマークされると、レベルが完了します。
  • ソリューションが存在すると仮定することができます。

あなたのコード

  • 目的:ボードを指定して、1つ以上の最短の解決策を見つけます。
  • 入力はボードを形成する正方形の位置のリスト(マークされた正方形とマークされていない正方形を区別する)であり、出力は動きのリストです。入力形式と出力形式は、人間が判読できて理解できるものである限り重要ではありません。
  • 勝利基準:各ボードで1分以内に見つかった最短のソリューションの移動数の合計。プログラムが特定のボードのソリューションを見つけられない場合、そのボードのスコアは(5 *正方形の数)です。
  • ソリューションをハードコードしないでください。あなたのコードは、以下の例として与えられたものだけでなく、入力としてどんなボードでも取ることができるはずです。

最初にゲームをプレイし、これらのいくつかを自分で試す機会を与えるために、ソリューションはネタバレに隠されています。また、それぞれについて以下のソリューションが1つだけ提供されます。

Sはバニーの開始広場(東向き)、#マークのない広場、Oマークのある広場です。動きの場合、私の表記はF=前方へのホップ、L=左折、R=右折であり、時間LOOP(<num>){<moves>}を反復<num>して<moves>毎回行うループを示します。ループが最小数を超えて何度でも実行できる場合は、<num>省略できます(つまり、無限大が機能します)。

レベル1:

S##

FF

レベル2:

S##
  #
  #

ループ(2){FFR}

レベル3:

S##
# #
###

ループ{FFR}

レベル4:

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

LOOP {F LOOP(7){FL}}(DJMcMayhemにより発見)

レベル5:

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

LOOP(18){LOOP(10){FR} L}
ソース:Reddit

レベル6:

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

ループ{ループ(3){F} L}

巨大なボード:(現在不明な最短の解決策)

12x12:

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

レベル5だが、はるかに大きい:

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

より穴あきボード:

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

そして

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

最後に、非対称性はお尻の本当の痛みになる可能性があります。

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

そして

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


「1つまたは複数の最短の解決策を見つける」停止する問題がこれを禁止すると思った
Leaky Nun

@Leaky Nunこれは停止の問題とは関係ありません。これはグラフ検索です
WhatToDo

しかし、ループは許可されています...
リーキー修道女

4
ボードは有限であるため、適用されないと思います。ループごとに、永久に実行されるか、停止します。内部にループのないループは、反復回数の引数が削除された場合にのみ永久にループします。その場合、ボードの状態の数が有限であるため、ループが状態の繰り返しを開始することが保証され、確認することができます。
WhatToDo

回答:


12

Python 3、67トークン

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'))

このプログラムは単一の入力ボードをテストしますが、このテストドライバーの方が便利だと思います。すべてのボードを同時にテストし、その解決策を見つけるのにかかった時間を印刷します。マシン(Intel i7-7700KクアッドコアCPU @ 4.20 GHz、16.0 GB RAM)でそのコードを実行すると、次の出力が得られます。

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

この最後のテストは、わずかな制限の下でやっときしみ音を立てます。

バックグラウンド

これは私がこれまでに答えた中で最も楽しい課題の1つでした!私は爆発パターンのハンティングを行い、物事を削減するためのヒューリスティックを探しました。

一般に、ここでPPCGについては、比較的簡単な質問に答える傾向があります。タグは、私の言語に一般的に非常に適しているため、特に気に入っています。ある日、約2週間前、バッジを見ていましたが、リバイバルバッジを手に入れたことがないことに気付きました。だから私は未回答を調べたタブで何かが目を引くかどうかを確認し、この質問を見つけました。費用に関係なく答えることにした。最終的には思っていたよりも少し難しくなりましたが、最終的には自慢していると言える強引な答えを得ました。しかし、私は通常、1つの回答に1時間程度しかかからないので、この課題は完全に標準から外れています。この答えは、慎重に追跡していませんでしたが、最終的にこの段階に到達するまでに2週間強と少なくとも10以上の作業を要しました。

最初の反復は、純粋なブルートフォースソリューションでした。次のコードを使用して、長さ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

この時点で、考えられるすべての回答をテストするのは遅すぎると確信していたので、使用するのが遅すぎました。テストケース12では、このアプローチを使用して3,000秒以上かかりました。これは明らかに非常に遅すぎます。しかし、この情報と一連のコンピューターサイクルを使用して、すべてのボードに短いソリューションをブルートフォースすることで、新しいパターンを見つけることができました。見つかったほとんどすべてのソリューションは、一般的に次のように見えることに気付きました。isSnippetRedundantは短いスニペットで記述できるスニペットを除外していました。たとえば["F", "F", "F"]、でまったく同じ効果が得られるため、スニペットの生成を拒否します[Loop(3, ["F"])。したがって、長さ3のスニペットをテストするポイントに到達した場合、長さ3のスニペットは現在のボードを解決できないことがわかります。これは良いニーモニックの多くを使用しますが、最終的だったメーリングリストの

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

いくつかのレイヤーを深くネストし、各側の単一のcomはオプションです。これは、次のようなソリューションを意味します。

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

表示されません。実際、3つ以上の非ループトークンのシーケンスはありませんでした。これを利用する1つの方法は、これらのすべてを除外し、それらをテストすることではありません。しかし、それらを生成することは依然として無視できない時間であり、このように数百万のスニペットをフィルタリングすることはほとんど時間をカットしませんでした。代わりに、このパターンに従ってスニペットのみを生成するようにコードジェネレーターを大幅に書き換えました。擬似コードでは、新しいジェネレーターは次の一般的なパターンに従います。

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>]

これにより、最長のテストケースが140秒に短縮されました。これはとんでもない改善です。しかし、ここからは、改善する必要のあることがまだありました。冗長/無益なコードをより積極的に除外し、コードが以前にテストされたかどうかを確認し始めました。これはさらにそれを削減しましたが、それは十分ではありませんでした。結局、欠けていた最後のピースはループカウンターでした。高度なアルゴリズム(ランダムな試行錯誤を読んでください)を通じて、ループを実行できる最適な範囲は[3-8]であると判断しました。しかし、そこには大きな改善があります:[loop(8, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]それがボードを解決できないことがわかっている場合、それは絶対にありません[loop(3, [loop(8, ['F', loop(5, ['F', 'L'])]), 'L'])]または、3〜7のループカウントで解決できます。したがって、3〜8のすべてのループサイズを繰り返すのではなく、外側のループのループカウントを最大に設定します。これにより、検索スペースがの係数で削減されmaxLoop - minLoopます。この場合は6です。

これは大いに役立ちましたが、スコアを膨らませてしまいました。前にブルートフォースで見つけた特定のソリューションを実行するには、より大きなループ数が必要です(たとえば、ボード4および6)。したがって、外側のループカウントを8に設定するのではなく、外側のループカウントを17に設定します。これは、高度なアルゴリズムによって計算される魔法の数です。一番外側のループのループカウントを増やしてもソリューションの有効性に影響しないため、これを実行できることがわかっています。このステップにより、実際に最終スコアが13減少しました。簡単なステップではありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.