Як я міг реалізувати щось на кшталт крафтової сітки Minecraft?


55

Система ремесла в Minecraft використовує сітку 2x2 або 3x3. Ви розміщуєте інгредієнти на сітці, і якщо ви поставите потрібні інгредієнти у потрібному шаблоні, це активує рецепт.

Деякі цікаві моменти щодо дизайну:

  • Деякі рецепти можуть обмінюватися певними інгредієнтами на інші. Наприклад, для вибору використовують палички для вішалки, а для голови можуть використовувати дерев'яні дошки, бруківку, залізні злитки, золоті злитки або алмазні дорогоцінні камені.
  • Відносна позиція в рамках шаблону - це те, що має значення, а не абсолютна позиція в мережі. Тобто ви можете виготовити факел , розмістивши палицю та вугілля (або вугілля) правильним шаблоном у будь-якому з шести положень на сітці 3х3.
  • Візерунки можуть перевертатися горизонтально.

Я можу це переосмислити, але це здається цікавою проблемою пошуку / зменшення. Отже, як (або міг би) це працювати, алгоритмічно кажучи?


6
Дивовижне питання! Мені дуже цікаво! У мене вже є кілька ідей, але я поділюсь ними, як тільки повернусь додому.
jcora

Я насправді намагаюся написати копію Minecraft "якомога ближче". Я вже написав усі матеріали з інвентаря та рецептів, і я повинен сказати, що це працює дуже акуратно. Я задоволений чистим кодом. Однак написати це було непросто. Я написав про 10 класів, щоб все добре працювало на рецептах, сітках, інвентарі тощо. Коли знайду якийсь час, напишу відповідь.
Martijn Courteaux

Ви повинні подивитися, як Minecraft кодує свої рецепти майстерності.
Бредман175

Відповіді:


14

Ще одне рішення - використовувати дещо складне дерево. Вузли гілки на вашому дереві будуть створені ітерацією над рецептом (ще раз з використанням for (y) { for (x) }); це ваша стандартна структура дерев для книги. Ваш кінцевий вузол міститиме додаткову структуру ( Dictionary/ HashMap), яка відображає розміри рецептів.

По суті, ви збираєтеся це:

Дерево рецепту

Чорні вузли - це ваші гілки, які вказують тип предмета - червоні - це ваші листя (термінатори), які дозволяють диференціювати розмір / орієнтацію.

Щоб здійснити пошук по цьому дереву, ви спершу знайдете обмежувальне поле (як зазначено в моїй першій відповіді ), а потім повторіть вузли в тому ж порядку, що проходить по дереву, як і ви. Нарешті, ви просто шукаєте розмір у своєму Dictionaryабо, HashMapі ви отримаєте результат рецепту.

Тільки для ударів я пішов і реалізував це - що, мабуть, прояснить мою відповідь. Також : Я розумію, що це різна відповідь - і це правильно: це різне рішення.


Дуже прямо. Мені це подобається.
Девід Ейк

Я прийму це, тому що мені подобаються дерева, хеші та діаграми, які вирізають серцевину справи. Один погляд на діаграму, і я зрозумів ваш алгоритм майже одразу.
Девід Ейк

23

Ви повинні пам’ятати, що Minecraft використовує лише дуже невеликий набір можливих рецептів, тому нічого розумного не потрібно.

Це сказало, що я б робив - це знайти найменшу сітку, яка підходить (тобто ігнорувати порожні рядки та стовпці, щоб дізнатися, чи це 2x2 чи 3x3, або 2x3 (двері)). Потім перегляньте список рецептів такого розміру, просто перевіривши, чи тип елемента однаковий (тобто, в гіршому випадку 9 цілих порівнянь у minecraft, оскільки він використовує цілий ідентифікатор типу для елементів і блоків), і зупиняйтесь, коли знайдете відповідність.

Це також робить відносне положення предметів неважливим (ви можете помістити факел кудись на ремісничій сітці, і він буде працювати, оскільки він бачить це як коробка 1x2, а не 3x3 коробка, яка в основному порожня).

Якщо у вас є величезна кількість рецептів, так що проведення лінійного пошуку через можливі збіги займає тривалий час, можна було б сортувати список і здійснити двійковий пошук (O (log (N)) vs O (N)). Це призведе до додаткової роботи зі створення списку, але це можна зробити при запуску один раз і зберегти в пам’яті згодом.

Крім того, останнє, щоб дозволити гортання рецепта в горизонтальному простоті було б просто додати дзеркальну версію до списку.

Якщо ви хочете зробити це без додавання другого рецепта, ви можете перевірити, чи є в рецепті введення елемент [0,0] з вищим ідентифікатором, ніж у [0,2] (або [0,1] для 2x2, не потрібно перевіряти для 1x2, і якщо так, це відобразити його, якщо не продовжувати перевірку наступного рядка, поки ви не досягнете кінця. Використовуючи це, ви також повинні переконатися, що рецепти додаються в правильному повороті.


1
Мені було цікаво, чи мала кількість рецептів у Minecraft може не піддатися лінійному пошуку.
Девід Ейк

1
Це є, і в основному те, що я написав вище (з можливою оптимізацією бінарного пошуку). Однак це залишає кілька варіантів для виготовлення факелів, які ви можете помістити в основному в будь-якому місці. Спочатку отримавши розмір рецепта, вам не потрібно додавати 6 рецептів смолоскипів, і ви обмежите свій пошук лише тими, що мають правильний розмір, за один крок. - Отже, варіанти вашого пункту 2 групуються за розміром, перенесіть рецепт у кут або додайте більше дублюючих рецептів.
Ельва

15

Перевірити, чи певна конфігурація сітки відповідає певній рецептурі, просто, якщо ви кодуєте сітку 3x3 як рядок і використовуєте відповідність регулярного вираження . Прискорення погляду - це інша справа, про яку я поговорю наприкінці. Читайте далі для отримання додаткової інформації.

Крок 1) Кодування сітки як рядка

Просто введіть знак ідентифікатора для кожного типу комірок і об'єднайте все поряд з цим порядком:

123
456 => 123456789
789

І як конкретніший приклад, розглянемо рецепт палички, де W означає деревину, а E - порожню клітинку (ви можете просто використовувати порожній знак ''):

EEE
WEE => EEEWEEWEE
WEE

Крок 2) Зіставте рецепт за допомогою регулярного вираження (або String.Contes with a bit of obdelava the data)

Продовжуючи з наведеного вище прикладу, навіть якщо ми переміщуємо формацію навколо, у рядку все ще є візерунок (WEEW з E з обох сторін):

EEW
EEW => EEWEEWEEE
EEE

Тож де б ви не пересували палицю, вона все одно буде відповідати наступному регулярному виразу: /^E*WEEWE*$/

Регулярні вирази також дозволяють виконувати згадану вами умовну поведінку. Наприклад (складений рецепт), якщо ви хочете, щоб кирка з заліза або каменю дала такий же результат, тобто:

III    SSS
EWE or EWE
EWE    EWE

Ви можете поєднати обидва в регулярний вираз: /^(III)|(SSS)EWEEWE$/

Горизонтальні гортання також можна легко додати (використовуючи також оператор |).

Редагувати: У будь-якому разі, частина регулярних виразів не є суворо необхідною. Це лише один спосіб інкапсулювати проблему в один вираз, але для проблеми зі змінним розташуванням ви можете так само добре обрізати сітку рядка будь-яких пробілів (або Е в цьому прикладі) і зробити String.Contains (). А для проблеми з кількома інгредієнтами або дзеркальних рецептів ви можете просто обробити їх усіма кількома (тобто окремими) рецептами з однаковим результатом.

Крок 3) Швидкість пошуку

Що стосується зменшення пошуку, вам потрібно створити структуру даних, щоб групувати рецепти разом і допомагати в пошуку. Трактування сітки як рядка має і деякі переваги тут :

  1. Ви можете визначити "довжину" рецепта як відстань між першим не порожнім символом і останнім не порожнім символом. Простий Trim().Length()дав би вам цю інформацію. Рецепти можна групувати за довжиною і зберігати у словнику.

    або

    Альтернативним визначенням "довжина" може бути кількість не порожніх символів. Нічого іншого не змінюється. Ви також могли групувати рецепти за цими критеріями.

  2. Якщо точки № 1 недостатньо, рецепти також можуть бути додатково згруповані за типом першого інгредієнта, який відображається в рецепті. Це було б так само просто, як робити Trim().CharAt(0)(і захист від обрізання в результаті порожнього рядка).

Так, наприклад, ви зберігаєте рецепти у:

Dictionary<int, Dictionary<char, List<string>>> _recipes;

І виконайте пошук як щось подібне:

// A string encode of your current grid configuration 
string grid;

// Get length and first char in our grid
string trim = grid.Trim();
int length = trim.Length();
char firstChar = length==0 ? ' ' : trim[0];

foreach(string recipe in _recipes[length][firstChar])
{
    // Check for a match with the recipe
    if(Regex.Match(grid, recipe))
    {
        // We found a matching recipe, do something with it
    }
}

3
Це ... дуже дуже розумно.
Raveline

18
У вас є проблема, і ви хочете її вирішити за допомогою регулярних виразів? Зараз у вас є 2 проблеми :)
Кромстер каже, що підтримує Моніку

2
@Krom Я думаю, ви, мабуть, просто жартуєте, але якісь міркування за цим коментарем? Використання регулярного виразу - це лише стислий (відповідність сітці проти рецепту з одним рядком коду) та гнучкий (відповідає всім вимогам цієї проблеми) спосіб виконання відповідності набору даних (вміст сітки). Я не бачу суттєвих недоліків щодо прокрутки власного алгоритму відповідності, і це спрощує весь процес.
Девід Гувейя

2
@DavidGouveia, це лише фраза для людей, які не надто комфортні з Regex. Якщо вони вирішили вирішити проблему з регулярним виразом, тепер у них є дві проблеми: (1) фактична проблема, яку вони хочуть вирішити (2) написання шаблону регулярних
виразів

2
Повернувшись до "тепер у вас є дві проблеми" - Regex матиме проблеми, як тільки у вас буде більше елементів, ніж діапазон друку ASCII, якщо ваш процесор регулярного вибору не підтримує unicode, і ви можете гарантувати, що певні комбінації не відключать компілятор Nge regex. вгору Ви можете зібрати разом свій власний DFA (досить легко насправді), щоб відповідати шаблонам; але найкраще підходити під час прототипування буде регулярний вираз.
Джонатан Дікінсон

3

Я не можу вам сказати, як працює Minecraft один - хоча я впевнений, що якщо ви подивилися на MCP (якщо у вас є юридична копія Minecraft), ви могли б це дізнатися.

Я би реалізував це так:

  1. Майте деякий дисковий словник / хешмап ( B + Tree / SQL-Light ) - значення буде ідентифікатором елемента результату рецепта.
  2. Коли користувач щось розробляє, просто знайдіть перший рядок / стовпець з елементом НЕЗАЛЕЖНО ; поєднайте їх у зміщення.
  3. Знайдіть останній рядок / стовпець із елементом, знову ж таки, незалежно.
  4. Обчисліть ширину та висоту елементів та додайте їх до клавіші.
  5. Перевірте діапазон обмежувального поля, який ви щойно обчислили, і додайте ідентифікатор кожного елемента (використовуючи звичайний for (y) { for (x) }).
  6. Подивіться ключ в базі даних.

Так, наприклад, скажімо, що у нас є два інгредієнти; X і Y і пробіли *. Візьміть такий рецепт:

**X
**Y
**Y

Спочатку опрацьовуємо обмежувальний ящик, поступаючись (2,0)-(2,2). Тому наш ключ виглядатиме так [1][3](1 ширина, 3 висота). Далі ми переводимо цикл на кожен елемент у вікні обмеження та додаємо ідентифікатор, таким чином ключ стає [1][3][X][Y][Y]- ви потім шукаєте це у своєму словнику / БД, і отримаєте результат цього рецепта.

Щоб пояснити незалежність на кроці 2 більш чітко, розглянемо наступний рецепт:

*XX
XXX
XX*

Вгорі / зліва чітко 0,0 - однак перший елемент, з яким ти зазвичай стикаєшся, буде або 0,1, або 1,0 (залежно від циклу). Однак якщо ви знайдете перший не порожній стовпець, а також перший не порожній рядок і поєднаєте ці координати, ви отримаєте 0,0 - те саме головне поширюється на нижню / праву частину обмежувального поля.


У сховищі Bukkit немає коду Minecraft, вам знадобиться MCP (і копія Minecraft)
BlueRaja - Danny Pflughoeft

Це не просто зворотний варіант вашої іншої відповіді?
Девід Ейк

@DavidEyk (це моя перша відповідь) насправді, це більше залежить від структури хешмапу (будь то велика таблиця чи пам'ять). Причина, коли я знову відповів, полягає в тому, що мене турбували хеш-зіткнення, що призводять до низької продуктивності - принаймні, у хеш-мапі пам'яті. Моя інша відповідь краще працювала б для більшої кількості елементів та структури пам'яті - де це було б краще, якби це було в SQL / тощо.
Джонатан Дікінсон

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.