도트와 박스를위한 가장 빠른 플레이어


16

도전 과제는 고전적인 연필과 종이 게임 점과 상자에 대한 솔버를 작성하는 것 입니다. 코드는 두 개의 정수 mn보드 크기를 지정하는 입력 값을 가져야 합니다.

비어있는 점 그리드로 시작하여 플레이어는 두 개의 결합되지 않은 인접한 점 사이에 단일 가로 또는 세로 선을 추가하여 회전합니다. 1 × 1 박스의 네 번째 측면을 완료 한 플레이어는 한 포인트를 획득하고 또 한 번 회전합니다. (점은 일반적으로 이니셜과 같이 플레이어의 식별 표시를 상자에 배치하여 기록됩니다). 더 이상 줄을 놓을 수 없으면 게임이 종료됩니다. 게임의 승자는 가장 많은 점수를 얻은 플레이어입니다.

여기에 이미지 설명을 입력하십시오

당신은 그 중 하나를 가정 할 수 n = m또는 n = m - 1m적어도 2입니다.

도전은 solve1 분 안에 가능한 가장 큰 Dots and Boxes 게임입니다. 게임의 크기는 간단 n*m합니다. 코드의 출력은해야한다 win, draw또는 lose어떤 두 선수가 최적의 재생 가정 첫번째 선수에 대한 결과되어야한다.

쉽게 설치 및 무료 도구를 사용하여 우분투에서 코드를 컴파일 / 실행할 수 있어야합니다. 1 분 안에 시간과 함께 컴퓨터에서 해결할 수있는 가장 큰 영역으로 점수를보고하십시오. 그런 다음 내 컴퓨터에서 코드를 테스트하고 순위가 매겨진 항목 테이블을 만듭니다.

타이 브레이크의 경우 1 분 안에 풀 수있는 가장 큰 크기의 보드에서 가장 빠른 코드가 우승자가됩니다.


출력 된 코드가이기거나지는 것이 아니라 실제 점수도 출력하는 것이 좋습니다. 이것은 정확성을 확인합니다.


2
minimax를 사용해야합니까?
qwr

@qwr 다른 옵션에 대해 알려주세요.

잠깐,이 게임에서 그리드 크기만을 기반으로 예측 가능한 승자가 있습니까?
찰스

@Charles 예, 두 선수 모두 최적으로 플레이하는 경우 가능합니다.

1
@PeterTaylor 두 점을 얻었지만 한 번만 더 회전한다고 생각합니다.

답변:


15

C99-0.084 초의 3x3 보드

편집 : 코드를 리팩터링하고 결과를 좀 더 심층 분석했습니다.

추가 편집 : 대칭에 의한 가지 치기가 추가되었습니다. 이것은 4 개의 알고리즘 구성을 만듭니다 : 알파-베타 가지 치기의 유무에 관계없이 대칭 X

가장 먼 편집 : 해시 테이블을 사용하여 메모를 추가하여 3x3 보드를 해결하는 것이 불가능 해졌습니다!

주요 특징들 :

  • 알파-베타 가지 치기를 통한 minimax의 간단한 구현
  • 매우 적은 메모리 관리 (유효한 이동의 dll 유지; 트리 검색에서 분기당 O (1) 업데이트)
  • 대칭으로 잘라내는 두 번째 파일. 분기당 O (1) 업데이트를 계속 달성합니다 (기술적으로 O (S), 여기서 S는 대칭 수입니다. 정사각형 보드의 경우 7 개, 정사각형이 아닌 보드의 경우 3 개).
  • 세 번째와 네 번째 파일은 메모를 추가합니다. 해시 테이블의 크기 ( #define HASHTABLE_BITWIDTH)를 제어 할 수 있습니다 . 이 크기가 벽의 수보다 크거나 같으면 충돌이없고 O (1) 업데이트가 보장됩니다. 작은 해시 테이블은 더 많은 충돌을 일으키고 약간 느려집니다.
  • -DDEBUG출력물로 컴파일

잠재적 개선 :

  • 첫 번째 편집에서 작은 메모리 누수 수정
  • 2 차 편집에 추가 된 알파 / 베타 가지 치기
  • 3 번째 편집에 추가 된 대칭 제거 (대칭은 아닙니다. 메모에 의해 처리 별도의 최적화로 유지됩니다.)
  • 4 번째 편집에 추가 된 메모
  • 현재 메모는 각 벽에 표시기 비트를 사용합니다. 3x4 보드에는 31 개의 벽이 있으므로이 방법은 시간 제약 조건에 관계없이 4x4 보드를 처리 할 수 ​​없습니다. X는 적어도 벽의 수만큼 큰 X- 비트 정수를 에뮬레이션하는 것이 개선 될 것입니다.

암호

구성 부족으로 인해 파일 수가 줄었습니다. 모든 코드 가이 Github Repository 로 옮겨졌습니다 . 메모 편집에서 makefile 및 테스트 스크립트를 추가했습니다.

결과

실행 시간의 로그 플롯

복잡성에 대한 메모

점과 상자에 대한 무차별 대입 방식은 매우 빠르게 복잡해 집니다.

와 보드를 고려 R행과 C열이 . 있다 R*C사각형, R*(C+1)수직 벽과 C*(R+1)수평 벽. 총계입니다 W = 2*R*C + R + C.

Lembik은 minimax로 게임 을 해결 해달라고 요청했기 때문에 게임 트리의 잎으로 이동해야합니다. 중요한 것은 순서가 중요하기 때문에 잘라내기를 무시합시다.

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 개는 사각형 이외의 보드에는 적용되지 않습니다. 각 벽에는 이러한 각 대칭에 대한 "쌍"에 대한 포인터가 있습니다. 턴에 들어가면 보드가 수평 대칭이면 각 수평 쌍 중 하나만 재생하면됩니다.

편집 편집 메모! 각 벽에는 고유 한 ID가 부여되는데, 이는 편리하게 지표 비트로 설정됩니다. n 번째 벽에는 id가 1 << n있습니다. 보드의 해시는 모든 벽의 OR입니다. 이것은 O (1) 시간의 각 분기에서 업데이트됩니다. 해시 테이블의 크기는로 설정됩니다 #define. 모든 테스트는 크기가 2 ^ 12로 실행되었습니다. 왜 그렇지 않습니까? 해시 테이블을 인덱싱하는 비트 (이 경우 12 비트)보다 많은 벽이있는 경우 최하위 12는 마스킹되어 인덱스로 사용됩니다. 충돌은 각 해시 테이블 인덱스에서 링크 된 목록으로 처리됩니다. 다음 차트는 해시 테이블 크기가 성능에 미치는 영향을 빠르고 정확하게 분석 한 것입니다. RAM이 무한한 컴퓨터에서는 항상 테이블의 크기를 벽 수로 설정합니다. 3x4 보드의 해시 테이블 길이는 2 ^ 31입니다. 아아 우리는 그런 사치를 가지고 있지 않다.

해시 테이블 크기의 영향

자르기로 돌아갑니다. 나무에서 검색을 중단하면 잎으로 내려 가지 않으면 서 많은 시간을 절약 할 수 있습니다 . '가지 치기 요소'는 우리가 방문해야 할 모든 가지의 일부입니다. Brute-force의 가지 치기 계수는 1입니다. 작을수록 좋습니다.

촬영 한 가지의 로그 그림

가지 치기 요인의 로그 그림


C와 같은 빠른 언어의 경우 23 초가 눈에 띄게 느린 것 같습니다.
qwr

알파 베타에서 소량의 가지 치기를 가진 무차별 대입. 그 23S는 의심 동의하지만, 다른 말로하면 ..이 일치하지 않을 것이라고 내 코드에서 그 신비를 어떤 이유가 표시되지 않습니다
wrongu

1
입력은 질문에 지정된 형식으로되어 있습니다. rows columns보드 크기를 지정하는 공백으로 구분 된 두 정수
wrongu

1
@Lembik 할 일이 남아 있다고 생각하지 않습니다. 나는이 미친 프로젝트를 끝냈습니다!
wrongu

1
나는 당신의 대답에 특별한 장소가 필요하다고 생각합니다. 나는 그것을 찾았고 3 x 3은 이전에 해결 된 가장 큰 문제 크기이며 코드는 거의 즉각적입니다. 3 x 4 또는 4 x 4를 풀 수 있다면 결과를 wiki 페이지에 추가하고 유명해질 수 있습니다 :)

4

파이썬-29s의 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

자바 스크립트-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입니다. 코드가 정확히 20ms 안에 해결할 수 있습니까?

"누군가가 정사각형을 얻는다면, 항상 턴 끝까지 연장됩니까?" -플레이어가 정사각형을 얻는다면 여전히 다음 턴으로 이동하지만 그 다음 턴은 같은 선수를위한 것입니다. "2x2는 3 점 x 3"-으악. 이 경우 내 점수는 1x1입니다.
rdans
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.