КОТ: Гомоку (пять подряд)

10

Гомоку или Пять в ряд - настольная игра, в которую играют два игрока на сетке с черными и белыми камнями. Тот, кто может разместить 5 камней подряд (по горизонтали, вертикали или диагонали), выигрывает игру.15×155

правила

В этом KoTH мы будем играть в правило Swap2, означающее, что игра состоит из двух этапов: на начальном этапе два игрока определяют, кто идет первым / кто играет черными, после чего они будут ставить один камень каждый раунд, начиная с игрока кто выбрал черный.

Начальный этап

Пусть игроки будут A & B, и A откроет игру:

  • Помещает два черных и один белый камень на доске
  • B может выбрать один из следующих трех ходов:
    • игрок B решает сыграть черным: начальная фаза окончена
    • игрок B решает поместить белый камень и играет белым: начальная фаза окончена
    • игрок B решает сыграть один черный и один белый камень: A выбирает цвет

Фаза игры

Каждый игрок кладет один камень своего цвета на игровое поле, начиная с игрока, который играет черным, это продолжается до тех пор, пока не останется свободного места для игры (в этом случае это ничья), или одному игроку удастся сыграть камней в игре. ряд (в этом случае тот игрок выигрывает).5

Ряд означает горизонтальный, вертикальный или диагональный. Победа - это победа - не имеет значения, сумел ли игрок забить более одного ряда.

Правила игры KoTH

  • каждый игрок играет друг против друга дважды:
    • Первоначально будет случайным образом решено, кто идет первым
    • в следующей игре игрок, который должен играть последним, идет первым
  • выигрыш стоит 2 очка, ничья 1 и проигрыш 0
  • цель - набрать как можно больше очков

Ваш бот

Чтобы сделать эту задачу доступной для максимально возможного количества языков, ввод / вывод будет осуществляться через stdin / stdout (на основе строки). Программа судья предложит программу, печатая строку для вашего бота стандартного ввода и бота напечатает одну строку в стандартный вывод .

Как только вы получите EXITсообщение, вам дается полсекунды, чтобы завершить запись в файлы, прежде чем судья завершит процесс.

хаотичность

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

аргументы

Бот получает два аргумента командной строки:

  1. имя оппонента
  2. семя для случайности

Состояние пользователя

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

Формат ввода / вывода

BOARD((X,Y),COLOR)XY[0,15)COLOR"B""W"

SPXY(X,Y)[0,15)|

На начальном этапе существует три вида сообщений:

Prompt (judge) -> Answer (bot)
"A" SP "[]"  -> XY XY XY
"B" SP BOARD -> "B" | "W" SP XY | XY XY
"C" SP BOARD -> "B" | "W"
  • Первое сообщение запрашивает три кортежа, первые два будут позициями черных камней, а третье позицией белых.
  • Второе сообщение запрашивает либо:
    • "B" -> выбрать черный
    • "W" SP XY -> выберите белый и поместите белый камень в XY
    • XY XY -> поместите два камня (первый черный и второй белый)
  • Последний просто спрашивает, какой цвет вы хотите играть

После этого начнется обычная игра, и сообщения станут намного проще

N BOARD -> XY

N0XY


Есть одно дополнительное сообщение, которое не ожидает ответа

"EXIT" SP NAME | "EXIT TIE"

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

Форматирование

Поскольку сообщения от бота могут быть декодированы без пробелов, все пробелы будут игнорироваться (например, (0 , 0) (0,12)обрабатываются так же, как (0,0)(0,12)). Сообщения от судьи содержат только пробел для разделения различных разделов (то есть, как отмечено выше с помощью SP), что позволяет разбить строку на пробелы.

Любой неверный ответ приведет к потере этого раунда (вы все равно получите EXITсообщение), см. Правила.

пример

Вот несколько примеров реальных сообщений:

A []
B [((0,0),"B"),((0,1),"W"),((14,14),"B")]
1 [((0,0),"B"),((0,1),"W"),((1,0),"B"),((1,1),"W"),((14,14),"B")]

судья

Вы можете найти программу судьи здесь : чтобы добавить в нее бота, просто создайте новую папку в botsпапке, поместите туда свои файлы и добавьте файл, metaсодержащий имя , команду , аргументы и флаг 0/1 (отключить / включить stderr ) каждый на отдельной строке.

Чтобы запустить турнир, просто запустите ./gomokuи отладьте один бот-запуск ./gomoku -d BOT.

Примечание: вы можете найти больше информации о том, как настроить и использовать судью в репозитории Github. Есть также три примера ботов ( Haskell , Python и JavaScript ).

правила

  • при каждой смене бота * турнир будет повторяться, и игрок с наибольшим количеством очков выигрывает (тай-брейк - первая подача)
  • Вы можете отправить более одного бота, если они не играют общей стратегии
  • вам не разрешается трогать файлы за пределами вашего каталога (например, манипулировать файлами других игроков)
  • если ваш бот вылетает или отправляет неверный ответ, текущая игра прекращается, и вы проигрываете этот раунд
  • хотя судья (в настоящее время) не применяет ограничение по времени на раунд, рекомендуется сократить время, затрачиваемое на проверку, поскольку проверка всех представлений может оказаться невозможной **
  • злоупотребление ошибками в программе судьи считается лазейкой

* Рекомендуется использовать Github для отдельной отправки вашего бота непосредственно в botsкаталог (и, возможно, его изменения util.sh)!

** Если это станет проблемой, о которой вы будете уведомлены, я бы сказал, что все, что ниже 500 мс (это очень много!) Должно быть хорошо на данный момент.

Чат

Если у вас есть вопросы или вы хотите , чтобы говорить об этом KOTH, не стесняйтесь присоединиться к чат !

ბიმო
источник
Связанный
ბიმო
Наличие пробелов в мета-пространстве в ваших примерах поражает воображение. Еще несколько примеров было бы неплохо.
Веска
@Veskah: Есть три примера ботов, я добавлю несколько примеров для сообщений.
ბიმო
@Veskah: добавлено несколько примеров. Btw. Вы также можете попробовать отладить пример бота, чтобы увидеть, в каком он будет формате, и проверить, что является правильным ответом.
ბიმო
Ты не давал разрешения на пуш, поэтому я не могу толкнуть своего бота на мерзавца
Kaito Kid

Ответы:

3

KaitoBot

Он использует очень грубую реализацию принципов MiniMax. Глубина поиска также очень мала, потому что в противном случае это займет слишком много времени.

Могу редактировать, чтобы улучшить позже.

Он также пытается играть черными, если это возможно, потому что Википедия, кажется, говорит, что у черных есть преимущество.

Я никогда не играл в Гомоку сам, поэтому я выставил первые три камня случайным образом из-за отсутствия лучшей идеи.

const readline = require('readline');
const readLine = readline.createInterface({ input: process.stdin });

var debug = true;
var myColor = '';
var opponentColor = '';
var board = [];
var seed = parseInt(process.argv[3]);

function random(min, max) {
    changeSeed();
    var x = Math.sin(seed) * 10000;
    var decimal = x - Math.floor(x);
    var chosen = Math.floor(min + (decimal * (max - min)));
    return chosen;
}

function changeSeed() {
    var x = Math.sin(seed++) * 10000;
    var decimal = x - Math.floor(x);
    seed = Math.floor(100 + (decimal * 9000000));
}

function KaitoBot(ln) {
    var ws = ln.split(' ');

    if (ws[0] === 'A') {
        // Let's play randomly, we don't care.
        var nums = [];
        nums[0] = [ random(0, 15), random(0, 15) ];
        nums[1] = [ random(0, 15), random(0, 15) ];
        nums[2] = [ random(0, 15), random(0, 15) ];
        while (nums[1][0] == nums[0][0] && nums[1][1] == nums[0][1])
        {
            nums[1] = [ random(0, 15), random(0, 15) ];
        }
        while ((nums[2][0] == nums[0][0] && nums[2][1] == nums[0][1]) || (nums[2][0] == nums[1][0] && nums[2][1] == nums[1][1]))
        {
            nums[2] = [ random(0, 15), random(0, 15) ];
        }
        console.log('(' + nums[0][0] + ',' + nums[0][1] + ') (' + nums[1][0] + ',' + nums[1][1] + ') (' + nums[2][0] + ',' + nums[2][1] + ')');
    }
    else if (ws[0] === 'B') {
        // we're second to play, let's just pick black
        myColor = 'B';
        opponentColor = 'W';
        console.log('B');
    }
    else if (ws[0] === 'C') {
        // the other player chose to play 2 stones more, we need to pick..
        // I would prefer playing Black
        myColor = 'B';
        opponentColor = 'W';
        console.log('B');
    }
    else if (ws[0] === 'EXIT') {
        process.exit();
    }
    else {
        board = [];
        var json = JSON.parse(ws[1].replace(/\(\(/g,'{"xy":[')
                .replace(/"\)/g,'"}')
                .replace(/\),/g,'],"colour":'));
        // loop over all XYs and make a board object I can use
        for (var x = 0; x < 15; x++) {
            var newRow = []
            for (var y = 0; y < 15; y++) {
                var contains = false;
                json.forEach(j => {
                    if (j.xy[0] == x && j.xy[1] == y) {
                        contains = true;
                        newRow[newRow.length] = j.colour;
                    }
                });
                if (!contains) {
                    newRow[newRow.length] = ' ';
                }
            }
            board[board.length] = newRow;
        }
        // If we never picked Black, I assume we're White
        if (myColor == '') {
            myColor = 'W';
            opponentColor = 'B';
        }
        var bestMoves = ChooseMove(board, myColor, opponentColor);
        var chosenMove = bestMoves[random(0, bestMoves.length)];
        console.log('(' + chosenMove.X + ',' + chosenMove.Y + ')');
    }
}

function IsSquareRelevant(board, x, y) {
    return (board[x][y] == ' ' && 
        ((x > 0 && board[x - 1][y] != ' ') 
        || (x < 14 && board[x + 1][y] != ' ') 
        || (y > 0 && board[x][y - 1] != ' ') 
        || (y < 14 && board[x][y + 1] != ' ')
        || (x > 0 && y > 0 && board[x - 1][y - 1] != ' ') 
        || (x < 14 && y < 14 && board[x + 1][y + 1] != ' ') 
        || (y > 0 && x < 14 && board[x + 1][y - 1] != ' ') 
        || (y < 14 && x > 0 && board[x - 1][y + 1] != ' ')));
}

function ChooseMove(board, colorMe, colorOpponent) {
    var possibleMoves = [];
    for (var x = 0; x < 15; x++) {
        for (var y = 0; y < 15; y++) {
            if (IsSquareRelevant(board, x, y)) {
                possibleMoves[possibleMoves.length] = {X:x, Y:y};
            }
        }
    }
    var bestValue = -9999;
    var bestMoves = [possibleMoves[0]];
    for (var k in possibleMoves) {
        var changedBoard = JSON.parse(JSON.stringify(board));
        changedBoard[possibleMoves[k].X][possibleMoves[k].Y] = colorMe;
        var value = analyseBoard(changedBoard, colorMe, colorOpponent, colorOpponent, 2);
        if (value > bestValue) {
            bestValue = value;
            bestMoves = [possibleMoves[k]];
        } else if (value == bestValue) {
            bestMoves[bestMoves.length] = possibleMoves[k];
        }
    }
    return bestMoves;
}

function analyseBoard(board, color, opponent, nextToPlay, depth) {
    var tBoard = board[0].map((x,i) => board.map(x => x[i]));
    var score = 0.0;
    for (var x = 0; x < board.length; x++) {
        var inARow = 0;
        var tInARow = 0;
        var opponentInARow = 0;
        var tOpponentInARow = 0;
        var inADiago1 = 0;
        var opponentInADiago1 = 0;
        var inADiago2 = 0;
        var opponentInADiago2 = 0;

        for (var y = 0; y < board.length; y++) {
            if (board[x][y] == color) {
                inARow++;
                score += Math.pow(2, inARow);
            } else {
                inARow = 0;
            }
            if (board[x][y] == opponent) {
                opponentInARow++;
                score -= Math.pow(2, opponentInARow);
            } else {
                opponentInARow = 0;
            }
            if (tBoard[x][y] == color) {
                tInARow++;
                score += Math.pow(2, tInARow);
            } else {
                tInARow = 0;
            }
            if (tBoard[x][y] == opponent) {
                tOpponentInARow++;
                score -= Math.pow(2, tOpponentInARow);
            } else {
                tOpponentInARow = 0;
            }

            var xy = (y + x) % 15;
            var xy2 = (x - y + 15) % 15;
            if (xy == 0) {
                inADiago1 = 0;
                opponentInADiago1 = 0;
            }
            if (xy2 == 0) {
                inADiago2 = 0;
                opponentInADiago2 = 0;
            }

            if (board[xy][y] == color) {
                inADiago1++;
                score += Math.pow(2, inADiago1);
            } else {
                inADiago1 = 0;
            }
            if (board[xy][y] == opponent) {
                opponentInADiago1++;
                score -= Math.pow(2, opponentInADiago1);
            } else {
                opponentInADiago1 = 0;
            }
            if (board[xy2][y] == color) {
                inADiago2++;
                score += Math.pow(2, inADiago2);
            } else {
                inADiago2 = 0;
            }
            if (board[xy2][y] == opponent) {
                opponentInADiago2++;
                score -= Math.pow(2, opponentInADiago2);
            } else {
                opponentInADiago2 = 0;
            }


            if (inARow == 5 || tInARow == 5) {
                return 999999999.0;
            } else if (opponentInARow == 5 || tOpponentInARow == 5) {
                return -99999999.0;
            }
            if (inADiago1 == 5 || inADiago2 == 5) {
                return 999999999.0;
            } else if (opponentInADiago1 == 5 || opponentInADiago2 == 5) {
                return -99999999.0;
            }
        }
    }

    if (depth > 0) {
        var bestMoveValue = 999999999;
        var nextNextToPlay = color;
        if (nextToPlay == color) {
            nextNextToPlay = opponent;
            bestMoveValue = -999999999;
        }
        for (var x = 0; x < board.length; x++) {
            for (var y = 0; y < board.length; y++) {
                if (IsSquareRelevant(board, x, y)) {
                    var changedBoard = JSON.parse(JSON.stringify(board));
                    changedBoard[x][y] = nextToPlay;
                    var NextMoveValue = (analyseBoard(changedBoard, color, opponent, nextNextToPlay, depth - 1) * 0.1);

                    if (nextToPlay == color) {
                        if (NextMoveValue > bestMoveValue) {
                            bestMoveValue = NextMoveValue;
                        }
                    } else {
                        if (NextMoveValue < bestMoveValue) {
                            bestMoveValue = NextMoveValue;
                        }
                    }
                }
            }
        }
        score += bestMoveValue * 0.1;
    }
    return score;
}

readLine.on('line', (ln) => {

    KaitoBot(ln);

});

РЕДАКТИРОВАТЬ: Сделано изменение семени динамически, потому что в противном случае, когда семена превысили 2 ^ 52, JavaScript не смог бы правильно обработать приращение

Кайто Кид
источник