Pull to refresh

Comments 43

Оффтоп: я без подсчетов из последней картинки вижу по крайней мере ещё несколько ясных ячеек:
Да, я комментом ниже исправил
UFO just landed and posted this here
UFO just landed and posted this here
Про мины я вообще-то ничего не говорил. Хотя их он определяет тоже. В данном случае он нашел три ячейки, там где мин НЕТ.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Да, вы правы. Однозначно, похоже, действительно только 2 мины и 3 пустых клетки можно открыть.
Забовное иследование.

«Сапер» подстраивался под игрока, иногда убирая из-под открываемой ячейки мину.

Если судить по ХР версии, то перестраивает поле когда Вы первым кликом попадаете на мину и больше никогда не меняет. Можно убедиться посмотрев заполнение поля в сапере для той же ХР, под спойлером c# код распределения цифр (для онлайне не пойдет — нужно знать расположение мин, но возможно кому-то будет интересно)
c# код.
private void OpenMap()
    {
        this.m_processes = Process.GetProcesses();
        foreach (Process process in this.m_processes)
        {
            if (process.ProcessName.ToLower() == "winmine")
            {
                this.wText = process.MainWindowTitle;
                int num = 6;
                int num2 = 0x31;
                int num3 = 0x10;
                int x = this.X;
                int y = this.Y;
                int[,] arr = this.Read(process);
                IntPtr hWnd = FindWindow(null, "Сапер");
                if (hWnd != IntPtr.Zero)
                {
                    this.m_procHandle = process.Handle;
                    if (GetWindowRect(hWnd, out this.rct))
                    {
                        Graphics graphics = Graphics.FromHwnd(hWnd);
                        Pen pen = new Pen(Color.Black, 1f);
                        for (int i = 0; i < y; i++)
                        {
                            for (int j = 0; j < x; j++)
                            {
                                if (arr[i, j] == 1)
                                {
                                    graphics.DrawEllipse(pen, new Rectangle(new Point((num + (num3 / 2)) + (num3 * j), (num2 + (num3 / 2)) + (num3 * i)), new Size(10, 10)));
                                    continue;
                                }
                                int num8 = this.Count(arr, i, j);
                                if (num8 > 0)
                                {
                                    Brush black = Brushes.Black;
                                    switch (((num8 - 1) % 3))
                                    {
                                        case 0:
                                            black = Brushes.Blue;
                                            break;

                                        case 1:
                                            black = Brushes.Green;
                                            break;

                                        case 2:
                                            black = Brushes.Red;
                                            break;
                                    }
                                    graphics.DrawString(num8.ToString(), this.Font, black, (PointF) new Point((num + (num3 / 2)) + (num3 * j), (num2 + (num3 / 2)) + (num3 * i)));
                                }
                            }
                        }
                    }
                }
                break;
            }
        }
    }

private int[,] Read(Process process)
{
    this.m_scanLength = 1;
    this.m_startAddress = 0x1005361;
    byte[] lpBuffer = new byte[this.m_scanLength];
    byte[] buffer2 = new byte[this.m_scanLength];
    int[,] numArray = new int[1, 1];
    if (process != null)
    {
        this.m_intptrProcess = OpenProcess(0x1f0fff, false, (uint) process.Id);
        ReadProcessMemory(this.m_intptrProcess, (IntPtr) 0x10056ac, buffer2, 1, out this.m_bytesRead);
        this.X = buffer2[0];
        buffer2 = new byte[1];
        ReadProcessMemory(this.m_intptrProcess, (IntPtr) 0x10056a8, buffer2, 1, out this.m_bytesRead);
        this.Y = buffer2[0];
        numArray = new int[this.Y + 2, this.X + 2];
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < this.Y; i++)
        {
            for (int j = 0; j < this.X; j++)
            {
                ReadProcessMemory(this.m_intptrProcess, (IntPtr) ((this.m_startAddress + (i * 0x20)) + j), lpBuffer, 1, out this.m_bytesRead);
                if (lpBuffer[0] == 0x8f)
                {
                    numArray[i, j] = 1;
                }
                builder.Append(lpBuffer[0].ToString() + "\t");
                lpBuffer = new byte[this.m_scanLength];
            }
        }
    }
    return numArray;
}

private int Count(int[,] arr, int i, int j)
{
    return (((((((this.Count2(arr, i - 1, j - 1) + this.Count2(arr, i - 1, j)) + this.Count2(arr, i - 1, j + 1)) + this.Count2(arr, i, j - 1)) + this.Count2(arr, i, j + 1)) + this.Count2(arr, i + 1, j - 1)) + this.Count2(arr, i + 1, j)) + this.Count2(arr, i + 1, j + 1));
}

 private int Count2(int[,] arr, int i, int j)
{
    if ((((i >= 0) && (j >= 0)) && (i < arr.GetLength(0))) && (j < arr.GetLength(1)))
    {
        return arr[i, j];
    }
    return 0;
}


Тестовое приложение.

Есть такая штука, как java.awt.Robot.
Ну это так, в тему — оказывается, очень многие не в курсе. Позволяет и мышкой возить программно и цвет пикселя по координатам определять.
Ага, именно его я и использовал.
А у нас на работе есть биологический вид бота ))) Уже не один год под конец рабочего дня типичная картина — сотрудник рьяно режется в сапера на миникарте. А напротив него сидит женщина и тоже режется, только в шахматы :-)
На одной из андроидовских версий сапёра есть бага в рандоме, приводящая к «полосному» минированию — глазами это видно, а робот и не догадается, что «где-то там есть полоса свободных».

В вопросах вероятностных эвристик надо добавить ещё вероятность для дальних клеток (то есть клеток, у которых нет свободных соседних).

Для рассчёта этой вероятности нужно посчитать число гарантировано «крайних» мин, вычесть его из оставшегося числа, и для всех глубинных полей посчитать вероятность нахождения там мины. Это будет «максимальная вероятность».

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

Эти вероятности надо учитывать, когда решать — открывать крайнее поле с вероятностью мины 0.14 или глубинное поле с максимальной вероятностью в 0.13 и минимальной в 0.10.
Хотя в статье я не отметил, но бот это тоже учитывает. Но считает глубинную вероятность несколько попроще, но ваш вариант вернее.
Что вы подразумеваете под гарантированно «крайним» полем? Бывают ситуации, когда нельзя точно сказать сколько мин возле открытых ячеек.


В вверху две двойки. Можно гарантировать, что в их окрестности есть 1 и только 1 мина.
И что это дает? На вашей же картинке в крайних полях может быть как 6 мин так и 7. Однозначно сказать нельзя.
соответственно, гарантированный минимум 6, максимум 7. так?
То есть вы хотите сказать, что глубокая вероятность должна быть равна (8-6)/(25-15)=0.2 или (8-7)/(25-15)=0.1, а не 8/25=0.32. Вы совершенно правы. А какой вариант лучше выбрать — с минимумом или с максимумом? Либо сравнить с первоначальной глубокой вероятностью, и если она лежит в пределах между минимумом и максимумом, то оставить ее, а если за пределами, то взять ближайшую к ней?
Вот что делать с диапазонами вероятностей — я не знаю. «Вероятность мины тут не более 0.3, не менее 0.1». Как его сравнивать с «не более 0.4 не менее 0.05» — не знаю.

Вопрос к гуру теорвера.
UFO just landed and posted this here
T(6) * (10 — 6) / U + T(7) * (10 — 7) / U
10 — число не отмеченных мин я полагаю? Тогда в этом примере оно равно 8.
UFO just landed and posted this here
Это в линуксовом минёре всегда можно логическими заключениями выяснить, есть мина или нет хотя бы в одной закрытой ячейке. А в виндовом иногда приходится жать наугад.
Аналогично, кстати, с пасьянсом Паук — в винде есть заведомо проигрышные расклады.
UFO just landed and posted this here
Во-первых, если внимательно читать статью, то можно заметить, что как раз эти 3 ячейки бот и находит. Во-вторых большое спасибо за предложения, попробую внедрить их в программу.
UFO just landed and posted this here
Просто когда включается LastTurns основной не включается. Смысла нет гонять оба.
На вскидку, мне кажется что объединять в такие небольшие группы — потеря информации. Группой должно быть множество связанных ячеек. 2 ячейки являются связанными если их соединяет открытая ячейка. ну и так далее. Потом, внутри группы делаем бэктрекинг, выделяем множество подходящих решений (подходящих под информацию с открытых клеток). Ищем ячейки которые являются минами или не минами сразу во всех подходящих множествах ну и собственно закрашиваем их.
Такой метод может помочь, когда нельзя сделать точный выбор из небольших групп. Ну и по прогнозам, должно работать достаточно быстро, даже если в группу будет входить 100 ячеек, хотя казалось бы возможных вариантов — 2^100.

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

То есть вы говорите об игре «Сапер», которая гарантированно выдает решаемые однозначно поля?
Можно ещё предварительно анализировать на стандартные 100% правила, полагаю, это должно улучшить показатели, так как не придется гадать, там где однозначно известен ответ.

Вот тут описал их http://pikabu.ru/story/eshchyo_odin_variant_prokhozhdeniya_sapera_3027351

Вот тут моя реализация этих правил на C#
https://www.dropbox.com/s/d4fype0rjfhuc5p/MinesFounder.rar?dl=0
Вообще-то этим как раз и занимается основной алгоритм (см. начало статьи)

Неправильно работает алгоритм по обработке пересечений групп.

Вот как решил алгоритм следующую ситуацию:

Он произвел помимо прочего такие преобразования:
(1273654, 3) - (1, 1) -> (273654, 2)
(273654, 2) X (87, 1) -> (7, 1), (23654, 1), (8, 0)
что вполне соответствует алгоритму, но очевидно неправильно.

Возможно просто какой-то нюанс преобразований автор в статье забыл упомянуть, но я не могу понять как исправить.

Доработал идею с группами.

Ввёл у группы понятие "минимально возможное кол-во мин" и "максимально возможное кол-во мин". При первоначальном создании групп заполняем одинаковым значением.

Переделал логику обработки пересечения групп, обобщив ее, тем самым избавившись от отдельного кейса "группа содержит группу". Из двух пересекающихся групп A и B создаем от одной до трех новых групп (но не плодя дубликаты), при этом старые группы не удаляем (это важно, но приходится запоминать кого с кем уже пересекали, чтобы повторно в будущем не пересекать):

A = {cells: ..., min: ..., max: ...}; // Массив ячеек, минимальное кол-во мин, максимальное кол-во мин
B = {cells: ..., min: ..., max: ...};

Добавляем новую группу C = {
cells: A.cells.getOverlap(B.cells), // Пересечение массивов ячеек
min: m0,
max: M0
}, где m0 и M0 считаем так:

m0a = min(max(A.min - (A.size - C.size), 0), C.size);
M0a = min(max(A.max, 0), C.size);
m0b = min(max(B.min - (B.size - C.size), 0), C.size);
M0b = min(max(B.max, 0), C.size);
m0 = max(m0a, m0b);
M0 = min(M0a, M0b);

Если разность массивов ячеек A минус B не пуста, добавляем группу D = {
cells: A.cells.getSubtraction(B.cells),
min: m1,
max: M1
}, где m1 и M2 считаем так:

m1 = min(max(A.min - C.max, 0), D.size);
M1 = min(max(A.max - C.min, 0), D.size);

Ну и соответственно если разность массивов ячеек B минус A не пуста, по аналогии добавляем группу E.

Также важно, при совпадении массива ячеек у двух групп удаляем не рандомную группу, а ту у которой min меньше и max больше, одновременно, иначе не удаляем никакую.

Sign up to leave a comment.

Articles