Как вам игровая сессия с 1000+ ходами в обычной ходилке? А такое вполне реально.
До этого я уже проанализировал одну немного бесячую настольную игру ходилку через эмуляции [1] [2]. В комментариях мне накидали кучу других запомнившихся игр с предложением и их потыкать. Ну вот я и потыкал. Для этого немного оптимизировал код эмулятора через javascript, чтобы он мог запускать по 100 миллионов игр. Скрипты выложены на гитхабе [3].

Вокруг света

Игровое поле
Игровое поле

В качестве механики большого отбрасывания (аналог чёрной дыры из прошлой статьи) я учитывал две позиции: 100->46, 107->37. А вот отбрасывание на начало 21->0 я не стал считать аналогом чёрной дыры, т.к. возврат на 21 ход примерно равнозначен обычным "стрелкам-назад". Статистика [4] вышла такая:

  • среднее число ходов 36;

  • максимальное число ходов 235;

  • минимальное число ходов 11;

  • число игр с попаданием хотя бы в одну отбрасывалку 54%, при этом игр с неравным числом попаданий в ловушки 43%;

  • вероятность проигрыша при более частом попадании в отбрасывалку 88%;

  • частота победы у первого игрока 50,85%.

График вероятности завершить игру за X ходов
График вероятности завершить игру за X ходов

Что интересного тут можно увидеть.
Плюсы:
- Красивая картинка, которую интересно разглядывать.
- Средняя длина игрового поля, очень долгая игровая сессия случается редко. Игра на 235 ходов случилась лишь однажды из 100 миллионов игр.
- Преимущество первого хода с 50,85% весьма небольшое.
Минусы:
- Мега отбрасывания, как всегда, подбешивают, но есть механика для камбека, так как оппонент сам может попасть в одну из двух ловушек у самого финиша.
- Если кого-то отбросило ловушкой чаще (что происходит с частотой 43%), то он проиграет с очень большой вероятностью: 88%.

Веселое путешествие

Игровое поле
Игровое поле

Здесь два отбрасывания в начало. При этом первая ловушка отбрасывает недалеко, поэтому её рассматривать как критическую я не стал. Поэтому ловушками я посчитал следующие комбинации: 63->0, 75->35. Статистика [5] вышла такая:

  • среднее число ходов 35;

  • максимальное число ходов 271;

  • минимальное число ходов 11;

  • число игр с попаданием хотя бы в одну отбрасывалку 50%, при этом игр с неравным числом попаданий в ловушки 41%;

  • вероятность проигрыша при более частом попадании в отбрасывалку 81%;

  • частота победы у первого игрока 50.78%.

График вероятности завершить игру за X ходов
График вероятности завершить игру за X ходов

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

Большое космическое путешествие (гребаный поезд)

Игровое поле
Игровое поле

Как подсказал, один из комментаторов AlexKoz1980, настоящее название этой игры - гребаный поезд. В качестве больших ловушек я считал за точки: 57, 70, 77, 88, 90. И судя по статистике такое название он полностью оправдывает [6].

  • среднее число ходов 102;

  • максимальное число ходов 1615;

  • минимальное число ходов 10;

  • число игр с попаданием хотя бы в одну отбрасывалку 92%, при этом игр с неравным числом попаданий в ловушки 70%;

  • вероятность проигрыша при более частом попадании в отбрасывалку 77%;

  • частота победы у первого игрока 50.14%.

График вероятности завершить игру за X ходов
График вероятности завершить игру за X ходов

Это самая несбалансированная игра из тех, что я видел. Помимо 5 самых опасных ловушек, тут есть ещё и мелкие ловушки, откатывающие на 1-2 этажа. Проблема в том, что после мелкой ловушки сбрасывается риск попадания в одну из опасных ловушек. И сохраняется он до самого конца. К 100-ому ходу становится уже неважно кто победит, лишь бы хоть кто-нибудь игру закончил.

Javascript для эмуляции использовался такой (можно запускать в консоли F12 в любой вкладке в любом браузере)

const finishStep = 93;
const countOfEmulatedGames = 100000000;
const bonusTurn = {
    9: true,
    24: true,
    43: true,
    56: true,
    82: true,
    84: true,
};
const moveBack = {};
const skipTurn = {
    2: true,
    8: true,
    11: true,
    21: true,
    23: true,
    28: true,
    29: true,
    32: true,
    39: true,
    44: true,
    45: true,
    49: true,
    58: true,
    59: true,
    68: true,
    73: true,
};
const instaDeath = {
    12: true,
};
const arrowMoves = {
    3: 5,
    4: 9,
    6: 27,
    13: 14,
    16: 18,
    19: 20,
    26: 46,
    30: 33,
    31: 36,
    34: 35,
    37: 38,
    40: 43,
    41: 46,
    51: 36,
    53: 54,
    57: 10,
    60: 47,
    61: 63,
    64: 67,
    65: 67,
    66: 46,
    69: 67,
    70: 10,
    72: 55,
    74: 78,
    75: 55,
    76: 78,
    77: 10,
    79: 78,
    83: 84,
    86: 87,
    88: 48,
    90: 10,
    91: 93,
};
const bigBack = {
    57: true,
    70: true,
    77: true,
    88: true,
    90: true,
};
let Stats = {
    totalGames: countOfEmulatedGames,
    iterationGames: 1000000,
    checkedGames: 0,
    turnsToGames: {},
    turnsToGamesPoints: {},
    catchedGames: 0,
    catchedGamesUnfair: 0,
    catchedMoreLoseGames: 0,
    firstPlayerWinCount: 0,
    totalTurns: 0,
    maxCountOfTurns: 0,
    minCountOfTurns: 999999,
};

function main() {
    let newCountOfGames = Math.min(Stats.checkedGames + Stats.iterationGames, Stats.totalGames);

    for (0; Stats.checkedGames < newCountOfGames; Stats.checkedGames++) {
        let game = emulateGame();

        Stats.totalTurns += game.turn;
        if (typeof Stats.turnsToGames[game.turn] === 'undefined') {
            Stats.turnsToGames[game.turn] = 0;
        }
        Stats.turnsToGames[game.turn]++;
        Stats.maxCountOfTurns = Math.max(Stats.maxCountOfTurns, game.turn);
        Stats.minCountOfTurns = Math.min(Stats.minCountOfTurns, game.turn);

        if (game.p1Catched > 0 || game.p2Catched > 0) {
            Stats.catchedGames++;
            if (game.p1Catched != game.p2Catched) {
                Stats.catchedGamesUnfair++;
            }
        }

        if (game.p1Catched > game.p2Catched && game.winner == 'p2') {
            Stats.catchedMoreLoseGames++;
        } else if (game.p1Catched < game.p2Catched && game.winner == 'p1') {
            Stats.catchedMoreLoseGames++;
        }

        if (game.winner == 'p1') {
            Stats.firstPlayerWinCount++;
        }
    }

    if (Stats.checkedGames >= Stats.totalGames) {
        console.log('Progress: 100% Done');

        Object.keys(Stats.turnsToGames).forEach(key => {
            Stats.turnsToGamesPoints[key] = 100*Stats.turnsToGames[key]/Stats.totalGames;
        });

        console.log('Count of games: ' + Stats.totalGames.toLocaleString());
        console.log('Average count of turns: ' + Math.round(100*Stats.totalTurns/Stats.totalGames)/100);
        console.log(JSON.stringify(Stats.turnsToGamesPoints));
        console.log('Max count of turns: ' + Stats.maxCountOfTurns);
        console.log('Min count of turns: ' + Stats.minCountOfTurns);
        console.log('--------------------');
        console.log('Percent of games with at least one big-back: ' + formatedRound(Stats.catchedGames/Stats.totalGames) + '%');
        console.log('Percent of unfair games with big-back: ' + formatedRound(Stats.catchedGamesUnfair/Stats.totalGames) + '%');
        console.log('If step to big-back more times then lose: ' + formatedRound(Stats.catchedMoreLoseGames/Stats.catchedGamesUnfair) + '%');
        console.log('--------------------');
        console.log('First player win rate: ' + formatedRound(Stats.firstPlayerWinCount/Stats.totalGames) + '%');
    } else {
        setTimeout(
            function() {
                console.log('Progress: ' + formatedRound(Stats.checkedGames/Stats.totalGames) + '%');
                main();
            },
            0
        );
    }
}

function emulateGame() {
    let game = {
        'p1': 0,
        'p2': 0,
        'winner': null,
        'p1Catched': 0,
        'p2Catched': 0,
        'turn': 0,
    }

    while(true) {
        game.turn++;

        game.p1 += getDice();
        game = checkMove(game, 'p1');

        if (game.p1 >= finishStep) {
            game.winner = 'p1';
            break;
        }

        game.p2 += getDice();
        game = checkMove(game, 'p2');

        if (game.p2 >= finishStep) {
            game.winner = 'p2';
            break;
        }
    }

    return game;
}

function checkMove(game, player) {
    let anotherPlayer = 'p1';
    if (player == anotherPlayer) {
        anotherPlayer = 'p2';
    }

    if (bigBack[game[player]]) {
        game[player + 'Catched']++;
    }

    if (bonusTurn[game[player]]) {
        game[player] += getDice();
        game = checkMove(game, player);
    }

    if (moveBack[game[player]]) {
        game[player] -= getDice();
        game = checkMove(game, player);
    }

    if (skipTurn[game[player]]) {
        game[anotherPlayer] += getDice();
        game = checkMove(game, anotherPlayer);
        game.turn++;
    }

    if (instaDeath[game[player]]) {
        game[player] = 0;
        //game[player + 'Catched']++; // skiped because zero return here almost at the start
    }

    if (typeof arrowMoves[game[player]] !== 'undefined') {
        game[player] = arrowMoves[game[player]];
    }

    return game;
}

function formatedRound(value) {
    return Math.round(10000*value)/100;
}

function getDice() {
    return Math.floor(Math.random() * 6 + 1);
}

main();

Космос от шестилетнего ребенка с дедушкой

А дальше идёт ходилка "Космическое приключение с чёрными дырами и кротовыми норами", созданная под руководством ребёнка. Картинка немного доработана, чтобы появились числа на шагах и легенда к игре.

Игровое поле
Игровое поле

Весьма типичная особенность новичка - циклопических размеров игровая карта, аж на 509 шагов. По первости часто кажется, что чем больше тем лучше, но это почти всегда не так.

Вторая особенность - наличие механики кротовой норы, в результате попадания в которую игрок моментально побеждает. На карте 4 кротовые норы и 4 чёрные дыры (возврат в начало).

Эмуляция [7] на 100 миллионов игр дало следующие результаты:

  • среднее число ходов 35;

  • максимальное число ходов 232;

  • минимальное число ходов 3;

  • число игр с попаданием хотя бы в одну чёрную дыру 65%, при этом игр с неравным числом попаданий в чёрные дыры 57%;

  • вероятность проигрыша при более частом попадании в чёрную дыру 54%;

  • побед через кротовую нору 83,5%;

  • частота победы у первого игрока 50,48%.

График вероятности завершить игру за X ходов
График вероятности завершить игру за X ходов

Что интересного тут можно увидеть.
Плюсы:
- Влияние чёрных дыр почти полностью нивелировано. 54% вероятности проиграть если ты попадал в чёрную дыру чаще оппонента - почти 50/50.
- Довольно часто игры заканчиваются до 20 ходов, быстрые игровые сессии это хорошо.
- Преимущество первого хода с 50,48% минимальное.
Минусы:
- Огромный путь в 509 шагов приводит к тому, что чаще всего игра очень сильно затягивается. Обычно это сильно утомляет. Рецепт простой - уменьшать карту до ~100 шагов и меньше.
- Победа почти всегда происходит за счёт попадания в кротовую нору. Поэтому, как вариант, следовало по максимуму использовать эту механику и многократно увеличить число кротовых нор при удалении от старта.

Заключение

Среди проверенных игр лишь гребаный поезд оказался сильно перекошенным. Остальные, на удивление, примерно одинаково проходятся за 35 ходов в среднем. Если вам известны другие безумные ходилки - скидывайте в комментариях. Если наберутся новые ещё более дикие, то я сделаю ещё подборку.

Источники

  1. GitHub. Javascript скрипты игровых эмуляторов.

  2. Насколько странный баланс в этой настолке с чёрной дырой на Хабре.

  3. Та же статья, но на Пикабу.

  4. GitHub. Анализ Вокруг света.

  5. GitHub. Веселое путешествие.

  6. GitHub. Гребаный поезд.

  7. GitHub. Анализ игры циклопических размеров с кротовыми норами.

Комментарии (7)


  1. Zara6502
    30.12.2022 13:31
    +2

    Весьма типичная особенность новичка - циклопических размеров игровая карта, аж на 509 шагов. По первости часто кажется, что чем больше тем лучше, но это почти всегда не так.

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

    Поэтому самые несбалансированные, как по мне, это Монополии/Менеджеры.


    1. qnok Автор
      30.12.2022 13:38

      В Монополию доигрывать часто неинтересно отстающему. Не очень весело наблюдать, как ты постепенно скатываешься в долговую яму и ничего не можешь с этим поделать, т.к. слишком сильно отстал от лидера.


  1. Pavgran
    30.12.2022 14:12
    +2

    Для такого рода игр можно получить точное решение с помощью цепей Маркова. Для каждой клетки поля расписывается вероятность попасть на каждую другую клетку поля. Затем можно использовать получившуюся матрицу в уравнениях, например, можно записать уравнение на ожидаемое количество ходов до каждой клетки. В решении такого рода уравнений будут учтены, в том числе, и бесконечные циклы (игрок постоянно ходит по кругу).

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

    Вероятности при условии более частого попадания в отбрасывалку считаются сложнее, но для них тоже потенциально можно получить точное решение. Здесь уже придётся (насколько я понимаю) хранить позицию каждого игрока и "превосходство по отбрасываниям". Матрица в этом случае будет большая и, возможно, за приемлемое время не удастся получить точного решения. Но в этом случае можно получить очень близкое к точному приближённое решение, используя в качестве состояния позицию игрока и количество отбрасываний вплоть до какого-то N, скажем, 50. Вероятности таких состояний будут очень маленькие, а следовательно, и погрешность результата.


    1. qnok Автор
      30.12.2022 14:25

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


    1. alkneu
      30.12.2022 19:55

      Ещё достаточно общий способ считать вероятности выигрыша,мат. ожидание (и другие статистические величины) в различных стохастических играх (цепи Маркова) - динамическое программирование (ДП/DP).

      Правда, строго говоря, чтобы применять DP, нужна некая монотонность/упорядоченность состояний игры (нет циклов). В общем случае - да, надо решить систему (линейных) уравнений.


    1. thevlad
      30.12.2022 23:50

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


  1. SadOcean
    30.12.2022 15:24
    +1

    Одинаковая длина сессии объясняется тем, что ее авторы вероятно ее считали.

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

    Но вообще тема хорошая и ваш пример интересный.

    Так то это обычное дело - погонять симуляцию.

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