Найшвидший гравець для крапок і коробок


16

Завдання полягає в тому, щоб написати розв'язувач для класичної гри з олівцями та папером « Крапки та коробки» . Ваш код повинен мати два цілі числа mі nяк вхідний, який визначає розмір плати.

Починаючи з порожньої сітки крапок, гравці роблять по черзі, додаючи єдину горизонтальну або вертикальну лінію між двома суміжними суміжними точками. Гравець, який завершив четверту сторону коробки розміром 1 × 1, заробляє одну точку і робить інший поворот. (Бали, як правило, записуються, розміщуючи в полі ідентифікаційний знак гравця, наприклад, початковий). Гра закінчується, коли більше рядків не можна розміщувати. Переможець гри - гравець з найбільшою кількістю очок.

введіть тут опис зображення

Ви можете припустити, що або n = mабо, n = m - 1і mпринаймні, 2.

Завдання полягає в solveмаксимально можливій грі "Дотс і Коробки" за лічені хвилини. Розмір гри просто n*m. Вихід вашого коду повинен бути win, drawабо loseякий повинен бути результатом для першого гравця, припускаючи, що обидва гравці грають оптимально.

Ваш код повинен бути комбінований / запущений на ubuntu, використовуючи легко встановлені та безкоштовні інструменти. Повідомте про свій рахунок як про найбільшу площу, яку ви можете вирішити на своєму комп’ютері за 1 хвилину разом із часом. Потім я перевірю код на своєму комп’ютері і складу таблицю записів, упорядкованих за рангом.

У випадку тай-брейку виграш буде найшвидшим кодом на дошці найбільшого розміру, яку він може вирішити за менше хвилини.


Було б краще, якщо код виводить не просто виграш або програш, а й фактичний результат. Це робить для перевірки правильності правильності.


2
Чи потрібно використовувати мінімакс?
qwr

@qwr Чи можете ви повідомити мені, який інший варіант ви мали на увазі?

Зачекайте, у цій грі є передбачуваний переможець, заснований виключно на розмірі сітки?
Не те, що Чарльз

@Charles Так, якщо обидва гравці грають оптимально.

1
@PeterTaylor Я думаю, ви отримаєте два очки, але лише один додатковий хід.

Відповіді:


15

C99 - 3x3 плата за 0,084s

Редагувати: я відновив свій код і зробив глибший аналіз результатів.

Подальші зміни: додано обрізку симетріями. Це робить 4 конфігурації алгоритму: з або без симетрії X з або без альфа-бета обрізки

Найдальші редагування: Додано запам'ятовування за допомогою хеш-таблиці, нарешті досягти неможливого: вирішити 3x3 дошку!

Основні характеристики:

  • пряма реалізація minimax з альфа-бета обрізкою
  • дуже мало управління пам’яттю (підтримує dll дійсних кроків; O (1) оновлення на гілку в дереві пошуку)
  • другий файл з обрізкою симетріями. Все ще досягається оновлення O (1) на галузь (технічно O (S), де S - кількість симетрій. Це 7 для квадратних дощок і 3 для неквадратичних дощок)
  • третій і четвертий файли додають запам'ятовування. Ви маєте контроль над розміром хештеля ( #define HASHTABLE_BITWIDTH). Коли цей розмір більший або рівний кількості стін, він гарантує відсутність зіткнень та оновлень O (1). Менші хештелі матимуть більше зіткнень та будуть трохи повільнішими.
  • компілювати з -DDEBUGдля роздруківки

Потенційні поліпшення:

  • виправити невелику витік пам'яті, виправлену в першій редакції
  • альфа / бета обрізка додана у другій редакції
  • симетрії чорносливу додано в 3-й редакції (зауважте, що це не симетрії редакції обробляються запам'ятовуванням, так що залишається окрема оптимізація.)
  • запам'ятовування додано в 4 редакції
  • В даний час для меморіації використовується індикаторний біт для кожної стіни. Дошка 3х4 має 31 стіну, тому цей метод не міг обробляти дошки 4х4 незалежно від часових обмежень. вдосконаленням було б емуляція X-бітових цілих чисел, де X принаймні така велика, як кількість стінок.

Код

Через відсутність організації кількість файлів вийшла з рук. Весь код переміщено до цього сховища Github . У редагуванні запам'ятовування я додав сценарій makefile та тестування.

Результати

Часовий графік часу виконання

Примітки про складність

Підходи грубої сили до крапок і коробок дуже швидко складються .

Розгляньте дошку з Rрядками та Cстовпцями. Є R*Cплощі, R*(C+1)вертикальні стіни та C*(R+1)горизонтальні стіни. Це загаломW = 2*R*C + R + C .

Оскільки Лембік попросив нас розв’язати гру з мінімаксом, нам потрібно пройти до листя ігрового дерева. Зараз ігноруємо обрізку, тому що важливо - це порядки.

Є 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 в 742560 разів більше, ніж 2x2 . Якщо 2x2 потребує 20 секунд, консервативна екстраполяція прогнозує понад 100 днів часу виконання 2x3. Очевидно, що нам потрібно обрізати.

Аналіз обрізки

Я почав з додавання дуже простої обрізки за допомогою алфавіту альфа-бета. В основному, він припиняє пошук, якщо ідеальний противник ніколи не дасть йому своїх сучасних можливостей. "Гляньте, я виграю багато, якщо мій опонент дозволить мені отримати кожен квадрат!", - ніколи не думав, AI.

редагувати Я також додав обрізки на основі симетричних дощок. Я не використовую підхід до запам'ятовування, про всяк випадок, коли-небудь я додаю пам'ятку і хочу, щоб цей аналіз був окремим. Натомість це працює так: більшість ліній мають "симетричну пару" десь ще в сітці. Існує до 7 симетрій (горизонтальна, вертикальна, 180 обертання, 90 обертання, 270 обертання, діагональна та інша діагональна). Усі 7 стосуються квадратних дощок, але останні 4 не стосуються не квадратних дощок. Кожна стіна має вказівник на "пару" для кожної з цих симетрій. Якщо, переходячи в свою чергу, дошка горизонтально симетрична, тоді потрібно грати лише одну з кожної горизонтальної пари .

редагувати редагувати Пам'ятка! Кожна стіна отримує унікальний ідентифікатор, який я зручно встановив як бітний показник; на n-й стіні є ідентифікатор 1 << n. Тоді хеш дошки - це просто АБО всіх зіграних стін. Це оновлюється в кожній гілці за O (1) час. Розмір хештеля встановлений у #define. Усі тести проводили з розміром 2 ^ 12, бо чому б ні? Якщо стінок більше, ніж бітів, що індексують хешбел (12 біт у цьому випадку), найменш значущі 12 маскуються та використовуються як індекс. Зіткнення обробляються зв'язаним списком у кожному індексі хештибу. Наступний графік - мій швидкий і брудний аналіз того, як розмір хештету впливає на продуктивність. На комп'ютері з нескінченною оперативною пам’яттю ми завжди встановлювали б розмір таблиці на кількість стінок. Дошка розміром 3х4 матиме хешбіл довжиною 2 ^ 31. На жаль, у нас немає такої розкоші.

Ефекти розміру хешбеля

Добре, назад до обрізки .. Зупинивши пошук високо в дереві, ми можемо заощадити багато часу, не спускаючись до листя. "Фактор обрізки" - це частина всіх можливих гілок, яку нам довелося відвідати. Груба сила має коефіцієнт обрізки 1. Чим менше, тим краще.

Ділянка журналу з взятих гілок

Діаграма журналу факторів обрізки


Для 23-х років здається помітно повільним для швидкої мови, наприклад, як C. Чи ви жорстоко змушуєте?
qwr

Груба сила з невеликою кількістю обрізки з альфа-бета. Я погоджуюся, що 23-х є підозрілими, але я не бачу причин у своєму коді, щоб це було непослідовно. Іншими словами, її таємниця
неправильно

1
вхід форматується відповідно до запитання. два цілих числа, розділених пробілом, із rows columnsзазначенням розміру плати
неправильно

1
@Lembik Я не думаю, що залишається щось робити. Я закінчив з цим шаленим проектом!
неправильно

1
Я думаю, що ваша відповідь заслуговує на особливе місце. Я переглянув це, і 3 на 3 - це найбільший розмір проблеми, який коли-небудь вирішувався, і ваш код майже миттєвий для цього. Якщо ви зможете вирішити 3 на 4 або 4 на 4, ви можете додати результат на сторінку wiki та бути відомим :)

4

Пітон - 2х2 в 29-х

Перехресне повідомлення з пазлів . Не особливо оптимізований, але може стати корисною відправною точкою для інших учасників.

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

Можливість пройти до 18 секунд за допомогою pypy.

2

Javascript - дошка 1x2 за 20 мс

Демонстрація в Інтернеті доступна тут (попередження - дуже повільно, якщо більша за 1x2 з повною глибиною пошуку ): https://dl.dropboxusercontent.com/u/141246873/minimax/index.html

Був розроблений для оригінальних критеріїв виграшу (код гольфу), а не для швидкості.

Тестували в Google Chrome v35 на Windows 7.

//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 х 3 справді цікаво, коли переможець змінюється вперед і назад у міру збільшення глибини пошуку. Чи можу я перевірити, чи ваш мінімакс колись зупиняється на півдорозі? Що я маю на увазі, якщо хтось отримує квадрат, чи завжди він поширюється до кінця своєї черги?

2x2 - це 3 крапки на 3. Ви впевнені, що ваш код може вирішити це саме за 20 мс?

"якщо хтось отримує квадрат, чи завжди він поширюється до кінця своєї черги?" - Якщо гравець отримує квадрат, він все одно переходить до наступного ходу, але наступний хід - для того ж гравця, тобто вони отримують додатковий оборот для заповнення квадрата. "2x2 - це 3 крапки по 3" - Уопс. У такому випадку моя оцінка становить 1х1.
rdans
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.