ドットとボックスの最速プレーヤー


16

課題は、古典的な鉛筆と紙のゲームDots and Boxesのソルバーを書くことです。あなたのコードでは、2つの整数を取る必要がありますmし、nボードのサイズを指定する入力として。

空のドットグリッドから始めて、プレーヤーは交わり、隣接していない2つのドット間に単一の水平線または垂直線を追加します。1×1ボックスの4番目のサイドを完了したプレーヤーは1ポイントを獲得し、別のターンを取ります。(ポイントは通常、ボックスにプレーヤーの識別マーク(イニシャルなど)を置くことで記録されます)。これ以上行を配置できない場合、ゲームは終了します。ゲームの勝者は、最もポイントの多いプレーヤーです。

ここに画像の説明を入力してください

n = mまたはが少なくとも2であるn = m - 1と仮定できmます。

課題はsolve、1分以内で可能な最大のドットとボックスゲームです。ゲームのサイズは単純n*mです。コードの出力はである必要がありますwindrawまたは、lose両方のプレーヤーが最適にプレイすると仮定した場合の最初のプレーヤーの結果になります。

コードは、簡単にインストールできる無料のツールを使用して、ubuntu上でコンパイル/実行できる必要があります。時間とともに1分でコンピューターで解くことができる最大の領域としてスコアを報告してください。次に、コンピューターでコードをテストし、ランク順に並べられたエントリのテーブルを作成します。

タイブレークの場合、勝者は1分以内に解決できる最大サイズのボードで最速のコードになります。


出力されたコードが勝ち負けだけでなく、実際のスコアも出力した方が良いでしょう。これにより、正当性の健全性チェックが行われます。


2
ミニマックスを使用する必要がありますか?
qwr 14

@qwr他に考えていたオプションを教えてください。

待って、グリッドサイズのみに基づいてこのゲームに予測可能な勝者がいますか?
チャールズではない14

@Charlesはい、両方のプレイヤーが最適にプレイする場合。

1
@PeterTaylorあなたは2ポイントを獲得できると思いますが、余分なターンは1つだけです。

回答:


15

C99-0.084秒で3x3ボード

編集:コードをリファクタリングし、結果をさらに詳しく分析しました。

さらに編集:対称性によるプルーニングを追加しました。これにより、4つのアルゴリズム構成が作成されます。対称Xの有無にかかわらず、アルファ-ベータプルーニングの有無にかかわらず

最も遠い編集:ハッシュテーブルを使用したメモ化を追加し、最終的に不可能を達成しました:3x3ボードを解く!

主な機能:

  • アルファ-ベータプルーニングによるミニマックスの簡単な実装
  • メモリ管理はほとんどありません(有効な移動のdllを維持します;ツリー検索のブランチごとのO(1)更新)
  • 対称性によるプルーニングのある2番目のファイル。それでもブランチごとにO(1)の更新を達成します(技術的にはO(S)。Sは対称性の数です。これは、正方形のボードでは7、非正方形のボードでは3です)。
  • 3番目と4番目のファイルはメモ化を追加します。ハッシュテーブルのサイズ(#define HASHTABLE_BITWIDTH)を制御できます。このサイズが壁の数以上である場合、衝突やO(1)の更新は保証されません。ハッシュテーブルが小さいほど衝突が多くなり、わずかに遅くなります。
  • -DDEBUG印刷用にコンパイル

潜在的な改善:

  • 最初の編集で修正された小さなメモリリークを修正
  • 2番目の編集で追加されたアルファ/ベータプルーニング
  • 3番目の編集で追加された整理対称整理はメモ化によって処理されないため、別個の最適化のままであることに注意してください。)
  • 4番目の編集でメモ化を追加
  • 現在、メモ化では各壁にインジケータビットを使用しています。3x4ボードには31個の壁があるため、この方法は時間の制約に関係なく4x4ボードを処理できませんでした。改善は、Xが壁の数と少なくとも同じ大きさのXビット整数をエミュレートすることです。

コード

組織の欠如のため、ファイルの数は手に負えないほど成長しました。すべてのコードはこのGithubリポジトリに移動されました。メモ化の編集では、メイクファイルとテストスクリプトを追加しました。

結果

実行時間のログプロット

複雑さに関する注意

ドットとボックスに対するブルートフォースアプローチは、非常に急速に複雑さを増します。

R行とC列のあるボードを検討してください。ありR*C、正方形、R*(C+1)垂直壁、およびC*(R+1)水平壁が。それは合計ですW = 2*R*C + R + C

Lembikがミニマックスでゲームを解決するように要求したため、ゲームツリーのリーフに移動する必要があります。重要なのは桁違いであるため、今は剪定を無視しましょう。

W最初の動きにはオプションがあります。これらのそれぞれについて、次のプレイヤーはW-1残りの壁などをプレイできます。これにより、、SS = W * (W-1) * (W-2) * ... * 1またはの検索スペースが得られますSS = W!。階乗は巨大ですが、それはほんの始まりに過ぎません。サーチスペース内SSリーフノードの数です。この分析により関連するのは、行わなければならない決定の総数(つまり、ツリー内のブランチ の数B)です。ブランチの最初のレイヤーにはWオプションがあります。それらのそれぞれについて、次のレベルにはW-1などがあります。

B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!

B = SUM W!/(W-k)!
  k=0..W-1

いくつかの小さなテーブルサイズを見てみましょう。

Board Size  Walls  Leaves (SS)      Branches (B)
---------------------------------------------------
1x1         04     24               64
1x2         07     5040             13699
2x2         12     479001600        1302061344
2x3         17     355687428096000  966858672404689

これらの数字はばかげている。少なくとも彼らは、ブルートフォースコードが2x3ボードで永遠にハングするように見える理由を説明します。2x3ボードのサーチスペースは2x2の742560倍です。2x2が完了するのに20秒かかる場合、控えめな外挿では、2x3の実行時間が100日超えると予測さます。明らかに剪定する必要があります。

プルーニング分析

まず、アルファベータアルゴリズムを使用した非常に単純なプルーニングを追加しました。基本的に、理想的な相手が現在の機会を与えない場合、検索を停止します。「おいおい-対戦相手が私にすべての広場を手に入れることができるなら、私はたくさん勝ちます!」、AIは決して考えなかった。

編集対称ボードに基づいたプルーニングも追加しました。いつかメモ化を追加し、その分析を別々にしたい場合に備えて、メモ化アプローチを使用しません。代わりに、次のように機能します。ほとんどの行には、グリッド上のどこかに「対称ペア」があります。最大7つの対称性(水平、垂直、180回転、90回転、270回転、対角線、およびその他の対角線)があります。7つすべてが正方形のボードに適用されますが、最後の4つは正方形でないボードには適用されません。各壁には、これらの対称性それぞれの「ペア」へのポインタがあります。ターンに入って、ボードが左右対称の場合、各水平ペアのうち1つだけをプレイする必要があります。

編集編集メモ!各壁には一意のIDが割り当てられますが、これは便利なようにインジケータビットとして設定します。n番目の壁にはidがあり1 << nます。したがって、ボードのハッシュは、プレイされたすべてのウォールのORです。これは、O(1)時間の各ブランチで更新されます。ハッシュテーブルのサイズはに設定されます#define。すべてのテストはサイズ2 ^ 12で実行されました。理由はなぜですか?ハッシュテーブルをインデックス付けするビット(この場合は12ビット)よりも多くの壁がある場合、最下位の12がマスクされ、インデックスとして使用されます。衝突は、各ハッシュテーブルインデックスのリンクリストで処理されます。次のグラフは、ハッシュテーブルのサイズがパフォーマンスにどのように影響するかを簡単に分析したものです。RAMが無限のコンピューターでは、テーブルのサイズを壁の数に常に設定します。3x4ボードの場合、ハッシュテーブルの長さは2 ^ 31です。残念ながら、そんな贅沢はありません。

ハッシュテーブルサイズの影響

さて、剪定に戻りましょう。ツリーの上位検索を停止することにより、葉に落ちないことで多くの時間を節約できます。「プルーニングファクター」とは、私たちが訪問しなければならなかったすべての可能な枝の割合です。ブルートフォースの枝刈り係数は1です。小さいほど、優れています。

取得したブランチのログプロット

枝刈り係数のログプロット


Cのような高速言語の場合、23秒は著しく遅いようです。
qwr 14年

アルファベータから少量の枝刈りをしたブルートフォース。その謎、言い換えれば..私は、23Sが不審であることに同意し、私はそれが矛盾するだろうと私のコードのいずれかの理由が表示されない
wrongu

1
入力は質問で指定されたとおりにフォーマットされます。2スペースで区切られた整数rows columnsボードのサイズを指定
wrongu

1
@Lembikやるべきことはないと思います。このクレイジーなプロジェクトは完了です!
間違った14年

1
あなたの答えは特別な場所に値すると思います。私はそれを調べましたが、3 x 3はこれまでに解決された最大の問題サイズであり、コードはほぼ瞬時に解決されます。3

4

Python-29秒で2x2

パズルからのクロスポスト。特に最適化されていませんが、他の参加者にとって有用な出発点になる可能性があります。

from collections import defaultdict

VERTICAL, HORIZONTAL = 0, 1

#represents a single line segment that can be drawn on the board.
class Line(object):
    def __init__(self, x, y, orientation):
        self.x = x
        self.y = y
        self.orientation = orientation
    def __hash__(self):
        return hash((self.x, self.y, self.orientation))
    def __eq__(self, other):
        if not isinstance(other, Line): return False
        return self.x == other.x and self.y == other.y and self.orientation == other.orientation
    def __repr__(self):
        return "Line({}, {}, {})".format(self.x, self.y, "HORIZONTAL" if self.orientation == HORIZONTAL else "VERTICAL")

class State(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.whose_turn = 0
        self.scores = {0:0, 1:0}
        self.lines = set()
    def copy(self):
        ret = State(self.width, self.height)
        ret.whose_turn = self.whose_turn
        ret.scores = self.scores.copy()
        ret.lines = self.lines.copy()
        return ret
    #iterate through all lines that can be placed on a blank board.
    def iter_all_lines(self):
        #horizontal lines
        for x in range(self.width):
            for y in range(self.height+1):
                yield Line(x, y, HORIZONTAL)
        #vertical lines
        for x in range(self.width+1):
            for y in range(self.height):
                yield Line(x, y, VERTICAL)
    #iterate through all lines that can be placed on this board, 
    #that haven't already been placed.
    def iter_available_lines(self):
        for line in self.iter_all_lines():
            if line not in self.lines:
                yield line

    #returns the number of points that would be earned by a player placing the line.
    def value(self, line):
        assert line not in self.lines
        all_placed = lambda seq: all(l in self.lines for l in seq)
        if line.orientation == HORIZONTAL:
            #lines composing the box above the line
            lines_above = [
                Line(line.x,   line.y+1, HORIZONTAL), #top
                Line(line.x,   line.y,   VERTICAL),   #left
                Line(line.x+1, line.y,   VERTICAL),   #right
            ]
            #lines composing the box below the line
            lines_below = [
                Line(line.x,   line.y-1, HORIZONTAL), #bottom
                Line(line.x,   line.y-1, VERTICAL),   #left
                Line(line.x+1, line.y-1, VERTICAL),   #right
            ]
            return all_placed(lines_above) + all_placed(lines_below)
        else:
            #lines composing the box to the left of the line
            lines_left = [
                Line(line.x-1, line.y+1, HORIZONTAL), #top
                Line(line.x-1, line.y,   HORIZONTAL), #bottom
                Line(line.x-1, line.y,   VERTICAL),   #left
            ]
            #lines composing the box to the right of the line
            lines_right = [
                Line(line.x,   line.y+1, HORIZONTAL), #top
                Line(line.x,   line.y,   HORIZONTAL), #bottom
                Line(line.x+1, line.y,   VERTICAL),   #right
            ]
            return all_placed(lines_left) + all_placed(lines_right)

    def is_game_over(self):
        #the game is over when no more moves can be made.
        return len(list(self.iter_available_lines())) == 0

    #iterates through all possible moves the current player could make.
    #Because scoring a point lets a player go again, a move can consist of a collection of multiple lines.
    def possible_moves(self):
        for line in self.iter_available_lines():
            if self.value(line) > 0:
                #this line would give us an extra turn.
                #so we create a hypothetical future state with this line already placed, and see what other moves can be made.
                future = self.copy()
                future.lines.add(line)
                if future.is_game_over(): 
                    yield [line]
                else:
                    for future_move in future.possible_moves():
                        yield [line] + future_move
            else:
                yield [line]

    def make_move(self, move):
        for line in move:
            self.scores[self.whose_turn] += self.value(line)
            self.lines.add(line)
        self.whose_turn = 1 - self.whose_turn

    def tuple(self):
        return (tuple(self.lines), tuple(self.scores.items()), self.whose_turn)
    def __hash__(self):
        return hash(self.tuple())
    def __eq__(self, other):
        if not isinstance(other, State): return False
        return self.tuple() == other.tuple()

#function decorator which memorizes previously calculated values.
def memoized(fn):
    answers = {}
    def mem_fn(*args):
        if args not in answers:
            answers[args] = fn(*args)
        return answers[args]
    return mem_fn

#finds the best possible move for the current player.
#returns a (move, value) tuple.
@memoized
def get_best_move(state):
    cur_player = state.whose_turn
    next_player = 1 - state.whose_turn
    if state.is_game_over():
        return (None, state.scores[cur_player] - state.scores[next_player])
    best_move = None
    best_score = float("inf")
    #choose the move that gives our opponent the lowest score
    for move in state.possible_moves():
        future = state.copy()
        future.make_move(move)
        _, score = get_best_move(future)
        if score < best_score:
            best_move = move
            best_score = score
    return [best_move, -best_score]

n = 2
m = 2
s = State(n,m)
best_move, relative_value = get_best_move(s)
if relative_value > 0:
    print("win")
elif relative_value == 0:
    print("draw")
else:
    print("lose")

pypyを使用して最大18秒まで高速化できます。

2

Javascript-20msで1x2ボード

ここで利用可能なオンラインデモ(警告- 完全な検索深度で1x2より大きい場合は非常に遅い):https : //dl.dropboxusercontent.com/u/141246873/minimax/index.html

スピードではなく、元の勝利基準(ゴルフのコード)のために開発されました。

Windows 7上のGoogle Chrome v35でテスト済み。

//first row is a horizontal edges and second is vertical
var gameEdges = [
    [false, false],
    [false, false, false],
    [false, false]
]

//track all possible moves and score outcome
var moves = []

function minimax(edges, isPlayersTurn, prevScore, depth) {

    if (depth <= 0) {
        return [prevScore, 0, 0];
    }
    else {

        var pointValue = 1;
        if (!isPlayersTurn)
            pointValue = -1;

        var moves = [];

        //get all possible moves and scores
        for (var i in edges) {
            for (var j in edges[i]) {
                //if edge is available then its a possible move
                if (!edges[i][j]) {

                    //if it would result in game over, add it to the scores array, otherwise, try the next move
                    //clone the array
                    var newEdges = [];
                    for (var k in edges)
                        newEdges.push(edges[k].slice(0));
                    //update state
                    newEdges[i][j] = true;
                    //if closing this edge would result in a complete square, get another move and get a point
                    //square could be formed above, below, right or left and could get two squares at the same time

                    var currentScore = prevScore;
                    //vertical edge
                    if (i % 2 !== 0) {//i === 1
                        if (newEdges[i] && newEdges[i][j - 1] && newEdges[i - 1] && newEdges[i - 1][j - 1] && newEdges[parseInt(i) + 1] && newEdges[parseInt(i) + 1][j - 1])
                            currentScore += pointValue;
                        if (newEdges[i] && newEdges[i][parseInt(j) + 1] && newEdges[i - 1] && newEdges[i - 1][j] && newEdges[parseInt(i) + 1] && newEdges[parseInt(i) + 1][j])
                            currentScore += pointValue;
                    } else {//horizontal
                        if (newEdges[i - 2] && newEdges[i - 2][j] && newEdges[i - 1][j] && newEdges[i - 1][parseInt(j) + 1])
                            currentScore += pointValue;
                        if (newEdges[parseInt(i) + 2] && newEdges[parseInt(i) + 2][j] && newEdges[parseInt(i) + 1][j] && newEdges[parseInt(i) + 1][parseInt(j) + 1])
                            currentScore += pointValue;
                    }

                    //leaf case - if all edges are taken then there are no more moves to evaluate
                    if (newEdges.every(function (arr) { return arr.every(Boolean) })) {
                        moves.push([currentScore, i, j]);
                        console.log("reached end case with possible score of " + currentScore);
                    }
                    else {
                        if ((isPlayersTurn && currentScore > prevScore) || (!isPlayersTurn && currentScore < prevScore)) {
                            //gained a point so get another turn
                            var newMove = minimax(newEdges, isPlayersTurn, currentScore, depth - 1);

                            moves.push([newMove[0], i, j]);
                        } else {
                            //didnt gain a point - opponents turn
                            var newMove = minimax(newEdges, !isPlayersTurn, currentScore, depth - 1);

                            moves.push([newMove[0], i, j]);
                        }
                    }



                }


            }

        }//end for each move

        var bestMove = moves[0];
        if (isPlayersTurn) {
            for (var i in moves) {
                if (moves[i][0] > bestMove[0])
                    bestMove = moves[i];
            }
        }
        else {
            for (var i in moves) {
                if (moves[i][0] < bestMove[0])
                    bestMove = moves[i];
            }
        }
        return bestMove;
    }
}

var player1Turn = true;
var squares = [[0,0],[0,0]]//change to "A" or "B" if square won by any of the players
var lastMove = null;

function output(text) {
    document.getElementById("content").innerHTML += text;
}

function clear() {
    document.getElementById("content").innerHTML = "";
}

function render() {
    var width = 3;
    if (document.getElementById('txtWidth').value)
        width = parseInt(document.getElementById('txtWidth').value);
    if (width < 2)
        width = 2;

    clear();
    //need to highlight the last move taken and show who has won each square
    for (var i in gameEdges) {
        for (var j in gameEdges[i]) {
            if (i % 2 === 0) {
                if(j === "0")
                    output("*");
                if (gameEdges[i][j] && lastMove[1] == i && lastMove[2] == j)
                    output(" <b>-</b> ");
                else if (gameEdges[i][j])
                    output(" - ");
                else
                    output("&nbsp;&nbsp;&nbsp;");
                output("*");
            }
            else {
                if (gameEdges[i][j] && lastMove[1] == i && lastMove[2] == j)
                    output("<b>|</b>");
                else if (gameEdges[i][j])
                    output("|");
                else
                    output("&nbsp;");

                if (j <= width - 2) {
                    if (squares[Math.floor(i / 2)][j] === 0)
                        output("&nbsp;&nbsp;&nbsp;&nbsp;");
                    else
                        output("&nbsp;" + squares[Math.floor(i / 2)][j] + "&nbsp;");
                }
            }
        }
        output("<br />");

    }
}

function nextMove(playFullGame) {
    var startTime = new Date().getTime();
    if (!gameEdges.every(function (arr) { return arr.every(Boolean) })) {

        var depth = 100;
        if (document.getElementById('txtDepth').value)
            depth = parseInt(document.getElementById('txtDepth').value);

        if (depth < 1)
            depth = 1;

        var move = minimax(gameEdges, true, 0, depth);
        gameEdges[move[1]][move[2]] = true;
        lastMove = move;

        //if a square was taken, need to update squares and whose turn it is

        var i = move[1];
        var j = move[2];
        var wonSquare = false;
        if (i % 2 !== 0) {//i === 1
            if (gameEdges[i] && gameEdges[i][j - 1] && gameEdges[i - 1] && gameEdges[i - 1][j - 1] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j - 1]) {
                squares[Math.floor(i / 2)][j - 1] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
            if (gameEdges[i] && gameEdges[i][parseInt(j) + 1] && gameEdges[i - 1] && gameEdges[i - 1][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j]) {
                squares[Math.floor(i / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
        } else {//horizontal
            if (gameEdges[i - 2] && gameEdges[i - 2][j] && gameEdges[i - 1] && gameEdges[i - 1][j] && gameEdges[i - 1] && gameEdges[i - 1][parseInt(j) + 1]) {
                squares[Math.floor((i - 1) / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
            if (gameEdges[i + 2] && gameEdges[parseInt(i) + 2][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][parseInt(j) + 1]) {
                squares[Math.floor(i / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
        }

        //didnt win a square so its the next players turn
        if (!wonSquare)
            player1Turn = !player1Turn;

        render();

        if (playFullGame) {
            nextMove(playFullGame);
        }
    }

    var endTime = new Date().getTime();
    var executionTime = endTime - startTime;
    document.getElementById("executionTime").innerHTML = 'Execution time: ' + executionTime;
}

function initGame() {

    var width = 3;
    var height = 2;

    if (document.getElementById('txtWidth').value)
        width = document.getElementById('txtWidth').value;
    if (document.getElementById('txtHeight').value)
        height = document.getElementById('txtHeight').value;

    if (width < 2)
        width = 2;
    if (height < 2)
        height = 2;

    var depth = 100;
    if (document.getElementById('txtDepth').value)
        depth = parseInt(document.getElementById('txtDepth').value);

    if (depth < 1)
        depth = 1;

    if (width > 2 && height > 2 && !document.getElementById('txtDepth').value)
        alert("Warning. Your system may become unresponsive. A smaller grid or search depth is highly recommended.");

    gameEdges = [];
    for (var i = 0; i < height; i++) {
        if (i == 0) {
            gameEdges.push([]);
            for (var j = 0; j < (width - 1) ; j++) {
                gameEdges[i].push(false);
            }
        }
        else {
            gameEdges.push([]);
            for (var j = 0; j < width; j++) {
                gameEdges[(i * 2) - 1].push(false);
            }
            gameEdges.push([]);
            for (var j = 0; j < (width - 1) ; j++) {
                gameEdges[i*2].push(false);
            }
        }
    }

    player1Turn = true;

    squares = [];
    for (var i = 0; i < (height - 1) ; i++) {
        squares.push([]);
        for (var j = 0; j < (width - 1); j++) {
            squares[i].push(0);
        }
    }

    lastMove = null;

    render();
}

document.addEventListener('DOMContentLoaded', initGame, false);

デモは本当に素晴らしいです!検索の深さを増やすと勝者が前後に変化するため、3 x 3は非常に興味深いものです。あなたのミニマックスはターンの途中で止まりますか?つまり、誰かがスクエアを獲得した場合、それは常に自分のターンの終わりまで続くのでしょうか?

2x2は3ドットx 3です。コードで20ミリ秒で正確に解決できますか?

「誰かがスクエアを獲得した場合、それは常に自分のターンの終わりまで延長されますか?」-プレイヤーがスクエアを獲得した場合、それはまだ次のターンに移動しますが、その次のターンは同じプレイヤーのためのものです。「2x2は3ドットx 3」-おっと。その場合、私のスコアは1x1です。
ルダン14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.