Есть данные полного перебора этой позиции в игре реверси. Как их использовать для поиска лучшего хода?

В Реверси имеется определенная позиция, и все возможные комбинации записаны в словаре с ключами и значениями. Полный перебор не гарантирует идеального хода, так как соперник может изменить свою стратегию, даже если вы выбрали вариант с наибольшими очками. Ходы обозначены индексами: 0 — клетка A8, 1 — клетка B8

введите сюда описание изображения

(1, 2, 0, 'пропуск', 3, 'пропуск', 6) счет (36, 28)
(1, 2, 0, 'пропуск', 6, 'пропуск', 3) счет (36, 28)
(6, 'пропуск', 1, 2, 0, 'пропуск', 3) счет (36, 28)
(6, 'пропуск', 1, 2, 3, 'пропуск', 0) счет (36, 28)
(1, 3, 0, 'пропуск', 2, 'пропуск', 6) счет (35, 29)
(1, 3, 0, 'пропуск', 6, 'пропуск', 2) счет (35, 29)
(1, 3, 6, 'пропуск', 0, 'пропуск', 2) счет (35, 29)
(6, 'пропуск', 1, 3, 0, 'пропуск', 2) счет (35, 29)
(3, 2, 6, 'пропуск', 1, 'пропуск', 0) счет (34, 30)
(2, 1, 3, 'пропуск', 0, 'пропуск', 6) счет (33, 31)
(2, 1, 3, 'пропуск', 6, 'пропуск', 0) счет (33, 31)
(6, 'пропуск', 2, 0, 3, 'пропуск', 1) счет (33, 31)
(6, 'пропуск', 2, 1, 3, 'пропуск', 0) счет (33, 31)
(6, 'пропуск', 3, 2, 1, 'пропуск', 0) счет (33, 31)
(0, 1, 3, 2, 6, 'пропуск', 'пропуск') счет (32, 32)
(3, 2, 0, 1, 6, 'пропуск', 'пропуск') счет (32, 32)
(0, 1, 2, 3, 6, 'пропуск', 'пропуск') счет (30, 34)
(1, 2, 6, 0, 3, 'пропуск', 'пропуск') счет (30, 34)
(1, 3, 6, 'пропуск', 2, 0, 'пропуск') счет (30, 34)
(2, 1, 0, 3, 6, 'пропуск', 'пропуск') счет (30, 34)
(2, 1, 6, 3, 0, 'пропуск', 'пропуск') счет (30, 34)
(2, 3, 6, 0, 1, 'пропуск', 'пропуск') счет (30, 34)
(2, 3, 6, 1, 0, 'пропуск', 'пропуск') счет (30, 34)
(6, 'пропуск', 1, 3, 2, 0, 'пропуск') счет (30, 34)
(0, 1, 6, 'пропуск', 3, 2, 'пропуск') счет (29, 35)
(6, 'пропуск', 0, 1, 3, 2, 'пропуск') счет (29, 35)
(6, 'пропуск', 3, 2, 0, 1, 'пропуск') счет (29, 35)
(2, 3, 0, 1, 6, 'пропуск', 'пропуск') счет (28, 36)
(0, 1, 6, 'пропуск', 2, 3, 'пропуск') счет (27, 37)
(1, 2, 3, 0, 6, 'пропуск', 'пропуск') счет (27, 37)
(2, 0, 6, 3, 1, 'пропуск', 'пропуск') счет (27, 37)
(3, 2, 6, 'пропуск', 0, 1, 'пропуск') счет (27, 37)
(6, 'пропуск', 0, 1, 2, 3, 'пропуск') счет (27, 37)
(6, 'пропуск', 2, 1, 0, 3, 'пропуск') счет (27, 37)
(1, 3, 2, 0, 6, 'пропуск', 'пропуск') счет (25, 39)
(2, 3, 1, 0, 6, 'пропуск', 'пропуск') счет (25, 39)
(3, 2, 1, 0, 6, 'пропуск', 'пропуск') счет (25, 39)
(6, 'пропуск', 2, 3, 1, 0, 'пропуск') счет (25, 39)
(2, 0, 3, 1, 6, 'пропуск', 'пропуск') счет (24, 40)
(6, 'пропуск', 2, 3, 0, 1, 'пропуск') счет (23, 41)
(2, 0, 1, 3, 6, 'пропуск', 'пропуск') счет (22, 42)
(6, 'пропуск', 2, 0, 1, 3, 'пропуск') счет (22, 42)

В итоге из всех этих вариаций 100% выигрышные вот эти 5 из 42 считал на бумажке

(1, 2, 0, 'пропуск', 3, 'пропуск', 6) счет (36, 28)
(1, 2, 0, 'пропуск', 6, 'пропуск', 3) счет (36, 28)
(1, 3, 0, 'пропуск', 2, 'пропуск', 6) счет (35, 29)
(1, 3, 0, 'пропуск', 6, 'пропуск', 2) счет (35, 29)
(1, 3, 6, 'пропуск', 0, 'пропуск', 2) счет (35, 29)

Ответы (1 шт):

Автор решения: Pak Uula

Гм. Минимакс уже не преподают в современных университетах?

Коротко.

Лучшая игра для белых: [1, 3, 0, None, 2, None, 6], итоговый счёт (35, 29)

Решение: https://pastebin.com/DickndEz

Длинно.

Нужно построить дерево текущий ход + возможные продолжения и для каждого узла в дереве дать оценку позиции.

Если ход последний, то оценка очевидна - разница между числом белых и чёрных фишек.

Если ход белых, то в качестве оценки берётся минимум оценок дочерних поддеревьев: чёрные постараются выбрать продолжение, максимизирующее число чёрных фишек в итоге.

Если ход чёрных, то белые будут разыгрывать такое продолжение, которое максимизирует белые фишки. То есть оценка равна максимуму оценки продолжения.

Чтобы найти наилучшую игру для белых, нужно выбрать первый ход с максимальной оценкой. Соответственно, черные всякий раз будут выбирать ход с минимальной оценкой.

class Tree:
    """Дерево игры: ход и оценка хода. Дочерние узлы - возможные продолжения игры.
    Оценка хода - разница в счёте в наихудшем продолжении после данного хода."""

    def __init__(self, move: Move, score: Tuple[int] = None):
        self.move = move
        self.score = score
        self._minimax: int = None
        if score:
            # Если последний ход в игре, то оценка хода - итоговый счёт
            self._minimax = score[0] - score[1]
        self.children: List[Self] = []

    @property
    def minimax(self) -> int:
        """Оценка хода: разница в счёте в наихудшем продолжении после данного хода."""
        if self._minimax is not None:
            return self._minimax
        if not self.children:
            raise ValueError("Листовой узел, но итоговый счёт не задан.")
        if self.move.side == WHITE:
            # Чёрные выберут продолжение с минимальным счётом.
            return min(c.minimax for c in self.children)
        # Белые выберут продолжение с максимальным счётом
        return max(c.minimax for c in self.children)

    def add_child(self, child) -> Self:
        """Добавить дочернее дерево продолжения игры"""
        self.children.append(child)
        return self

    def get_by_minimax(self) -> Self:
        """Возвращает дочернее дерево с наилучшим продолжением игры."""
        minimax = self.minimax
        for child in self.children:
            if child.minimax == minimax:
                return child

    def best_game(self) -> Game:
        """Возвращает лучшую игру, начинающуюся с этого хода."""
        if not self.children:
            return Game([self.move.position], self.move.side, self.score)
        child = self.get_by_minimax()
        best_game = child.best_game()
        return Game(
            [self.move.position] + best_game.moves, self.move.side, best_game.score
        )

    def print_tree(self, level=0):
        """Печатает дерево игры"""
        print("| " * level, self.move, self.minimax)
        for child in self.children:
            child.print_tree(level + 1)

Служебные классы для представления игры и отдельного хода

WHITE = "W"
BLACK = "B"


class Move:
    """Ход в игре: позиция и сторона"""

    def __init__(self, position: int, side: str):
        self.position = position
        self.side = side

    def __hash__(self) -> int:
        return hash((self.position, self.side))

    def __eq__(self, other) -> bool:
        if isinstance(other, Move):
            return self.position == other.position and self.side == other.side
        return False

    def __repr__(self) -> str:
        return f"Move({self.position}, {self.side})"

    def __str__(self) -> str:
        return f"{self.side}:{self.position}"

    @property
    def opponent(self) -> str:
        """Цвет противника"""
        return WHITE if self.side == BLACK else BLACK


class Game:
    """Игра: последовательность ходов, сторона, делающая первый ход, и итоговый счёт"""

    def __init__(self, moves: Tuple[int], side: str, score: Tuple[int]):
        self.moves = moves
        self.side = side
        self.score = score

    def __len__(self) -> int:
        return len(self.moves)

    @property
    def head(self) -> Move:
        """Первый ход"""
        return Move(self.moves[0], self.side)

    @property
    def tail(self) -> Self:
        """Оставшиеся ходы"""
        return Game(self.moves[1:], self.opponent, self.score)

    @property
    def opponent(self) -> str:
        """Цвет противника"""
        return self.head.opponent

Построитель дерева

class TreeBuilder:
    """Построитель дерева игры из списка игр."""

    def __init__(self, move, games):
        self.games = games
        self.tree = Tree(move)
        self.games_by_move = defaultdict(list)
        self._build_tree()

    def _build_tree(self):
        for game in self.games:
            if len(game) == 1:
                self.tree.add_child(Tree(game.head, game.score))
            else:
                self.games_by_move[game.head].append(game.tail)
        for move, tails in self.games_by_move.items():
            self.tree.add_child(TreeBuilder(move, tails).tree)
        self.tree.children.sort(key=lambda c: c.move.position)

Ну и собственно анализ:

# список игр из задачи
GAMES = [
    Game((0, 1, 2, 3, 6), WHITE, (30, 34)),
    Game((0, 1, 3, 2, 6), WHITE, (32, 32)),
    Game((0, 1, 6, None, 2, 3), WHITE, (27, 37)),
    Game((0, 1, 6, None, 3, 2), WHITE, (29, 35)),
    Game((1, 2, 0, None, 3, None, 6), WHITE, (36, 28)),
    Game((1, 2, 0, None, 6, None, 3), WHITE, (36, 28)),
    Game((1, 2, 3, 0, 6), WHITE, (27, 37)),
    Game((1, 2, 6, 0, 3), WHITE, (30, 34)),
    Game((1, 3, 0, None, 2, None, 6), WHITE, (35, 29)),
    Game((1, 3, 0, None, 6, None, 2), WHITE, (35, 29)),
    Game((1, 3, 2, 0, 6), WHITE, (25, 39)),
    Game((1, 3, 6, None, 0, None, 2), WHITE, (35, 29)),
    Game((1, 3, 6, None, 2, 0), WHITE, (30, 34)),
    Game((2, 0, 1, 3, 6), WHITE, (22, 42)),
    Game((2, 0, 3, 1, 6), WHITE, (24, 40)),
    Game((2, 0, 6, 3, 1), WHITE, (27, 37)),
    Game((2, 1, 0, 3, 6), WHITE, (30, 34)),
    Game((2, 1, 3, None, 0, None, 6), WHITE, (33, 31)),
    Game((2, 1, 3, None, 6, None, 0), WHITE, (33, 31)),
    Game((2, 1, 6, 3, 0), WHITE, (30, 34)),
    Game((2, 3, 0, 1, 6), WHITE, (28, 36)),
    Game((2, 3, 1, 0, 6), WHITE, (25, 39)),
    Game((2, 3, 6, 0, 1), WHITE, (30, 34)),
    Game((2, 3, 6, 1, 0), WHITE, (30, 34)),
    Game((3, 2, 0, 1, 6), WHITE, (32, 32)),
    Game((3, 2, 1, 0, 6), WHITE, (25, 39)),
    Game((3, 2, 6, None, 0, 1), WHITE, (27, 37)),
    Game((3, 2, 6, None, 1, None, 0), WHITE, (34, 30)),
    Game((6, None, 0, 1, 2, 3), WHITE, (27, 37)),
    Game((6, None, 0, 1, 3, 2), WHITE, (29, 35)),
    Game((6, None, 1, 2, 0, None, 3), WHITE, (36, 28)),
    Game((6, None, 1, 2, 3, None, 0), WHITE, (36, 28)),
    Game((6, None, 1, 3, 0, None, 2), WHITE, (35, 29)),
    Game((6, None, 1, 3, 2, 0), WHITE, (30, 34)),
    Game((6, None, 2, 0, 1, 3), WHITE, (22, 42)),
    Game((6, None, 2, 0, 3, None, 1), WHITE, (33, 31)),
    Game((6, None, 2, 1, 0, 3), WHITE, (27, 37)),
    Game((6, None, 2, 1, 3, None, 0), WHITE, (33, 31)),
    Game((6, None, 2, 3, 0, 1), WHITE, (23, 41)),
    Game((6, None, 2, 3, 1, 0), WHITE, (25, 39)),
    Game((6, None, 3, 2, 0, 1), WHITE, (29, 35)),
    Game((6, None, 3, 2, 1, None, 0), WHITE, (33, 31)),
]

# Дерево игры
tree = TreeBuilder(Move(None, BLACK), GAMES).tree
for kid in tree.children:
    print(
        f"Если белые сделают ход {kid.move.position}, то разница в итоговом счёте будет {kid.minimax}"
    )

white_best = max(tree.children, key=lambda c: c.minimax)
game = white_best.best_game()
print("Лучшая игра для белых: ", game.moves, game.score)

for kid in tree.children:
    game = kid.best_game()
    print(
        f"Если белые сделают ход {kid.move.position}, то лучшая игра будет ",
        game.moves,
        game.score,
    )

Результат:

Если белые сделают ход 0, то разница в итоговом счёте будет 0
Если белые сделают ход 1, то разница в итоговом счёте будет 6
Если белые сделают ход 2, то разница в итоговом счёте будет -10
Если белые сделают ход 3, то разница в итоговом счёте будет 4
Если белые сделают ход 6, то разница в итоговом счёте будет 6
Лучшая игра для белых:  [1, 3, 0, None, 2, None, 6] (35, 29)
Если белые сделают ход 0, то лучшая игра будет  [0, 1, 3, 2, 6] (32, 32)
Если белые сделают ход 1, то лучшая игра будет  [1, 3, 0, None, 2, None, 6] (35, 29)
Если белые сделают ход 2, то лучшая игра будет  [2, 0, 6, 3, 1] (27, 37)
Если белые сделают ход 3, то лучшая игра будет  [3, 2, 6, None, 1, None, 0] (34, 30)
Если белые сделают ход 6, то лучшая игра будет  [6, None, 1, 3, 0, None, 2] (35, 29)

Для проверки вот дерево ходов с оценками

 B:None 6
|  W:0 0
| |  B:1 0
| | |  W:2 -4
| | | |  B:3 -4
| | | | |  W:6 -4
| | |  W:3 0
| | | |  B:2 0
| | | | |  W:6 0
| | |  W:6 -6
| | | |  B:None -6
| | | | |  W:2 -10
| | | | | |  B:3 -10
| | | | |  W:3 -6
| | | | | |  B:2 -6
|  W:1 6
| |  B:2 8
| | |  W:0 8
| | | |  B:None 8
| | | | |  W:3 8
| | | | | |  B:None 8
| | | | | | |  W:6 8
| | | | |  W:6 8
| | | | | |  B:None 8
| | | | | | |  W:3 8
| | |  W:3 -10
| | | |  B:0 -10
| | | | |  W:6 -10
| | |  W:6 -4
| | | |  B:0 -4
| | | | |  W:3 -4
| |  B:3 6
| | |  W:0 6
| | | |  B:None 6
| | | | |  W:2 6
| | | | | |  B:None 6
| | | | | | |  W:6 6
| | | | |  W:6 6
| | | | | |  B:None 6
| | | | | | |  W:2 6
| | |  W:2 -14
| | | |  B:0 -14
| | | | |  W:6 -14
| | |  W:6 6
| | | |  B:None 6
| | | | |  W:0 6
| | | | | |  B:None 6
| | | | | | |  W:2 6
| | | | |  W:2 -4
| | | | | |  B:0 -4
|  W:2 -10
| |  B:0 -10
| | |  W:1 -20
| | | |  B:3 -20
| | | | |  W:6 -20
| | |  W:3 -16
| | | |  B:1 -16
| | | | |  W:6 -16
| | |  W:6 -10
| | | |  B:3 -10
| | | | |  W:1 -10
| |  B:1 2
| | |  W:0 -4
| | | |  B:3 -4
| | | | |  W:6 -4
| | |  W:3 2
| | | |  B:None 2
| | | | |  W:0 2
| | | | | |  B:None 2
| | | | | | |  W:6 2
| | | | |  W:6 2
| | | | | |  B:None 2
| | | | | | |  W:0 2
| | |  W:6 -4
| | | |  B:3 -4
| | | | |  W:0 -4
| |  B:3 -4
| | |  W:0 -8
| | | |  B:1 -8
| | | | |  W:6 -8
| | |  W:1 -14
| | | |  B:0 -14
| | | | |  W:6 -14
| | |  W:6 -4
| | | |  B:0 -4
| | | | |  W:1 -4
| | | |  B:1 -4
| | | | |  W:0 -4
|  W:3 4
| |  B:2 4
| | |  W:0 0
| | | |  B:1 0
| | | | |  W:6 0
| | |  W:1 -14
| | | |  B:0 -14
| | | | |  W:6 -14
| | |  W:6 4
| | | |  B:None 4
| | | | |  W:0 -10
| | | | | |  B:1 -10
| | | | |  W:1 4
| | | | | |  B:None 4
| | | | | | |  W:0 4
|  W:6 6
| |  B:None 6
| | |  W:0 -6
| | | |  B:1 -6
| | | | |  W:2 -10
| | | | | |  B:3 -10
| | | | |  W:3 -6
| | | | | |  B:2 -6
| | |  W:1 6
| | | |  B:2 8
| | | | |  W:0 8
| | | | | |  B:None 8
| | | | | | |  W:3 8
| | | | |  W:3 8
| | | | | |  B:None 8
| | | | | | |  W:0 8
| | | |  B:3 6
| | | | |  W:0 6
| | | | | |  B:None 6
| | | | | | |  W:2 6
| | | | |  W:2 -4
| | | | | |  B:0 -4
| | |  W:2 -14
| | | |  B:0 2
| | | | |  W:1 -20
| | | | | |  B:3 -20
| | | | |  W:3 2
| | | | | |  B:None 2
| | | | | | |  W:1 2
| | | |  B:1 2
| | | | |  W:0 -10
| | | | | |  B:3 -10
| | | | |  W:3 2
| | | | | |  B:None 2
| | | | | | |  W:0 2
| | | |  B:3 -14
| | | | |  W:0 -18
| | | | | |  B:1 -18
| | | | |  W:1 -14
| | | | | |  B:0 -14
| | |  W:3 2
| | | |  B:2 2
| | | | |  W:0 -6
| | | | | |  B:1 -6
| | | | |  W:1 2
| | | | | |  B:None 2
| | | | | | |  W:0 2
→ Ссылка