Гонитва з перешкодами

 
           Яка повільна країна! — сказала Королева. — Ну, а тут,
чи знаєш, доводиться бігти з усіх ніг, щоб тільки залишитися
на тому ж місці! Якщо ж хочеш потрапити в інше місце, тоді
потрібно бігти щонайменше вдвічі швидше! 
 
                        Льюїс Керролл "Аліса в Задзеркаллі


Сьогодні, я хочу розповісти про дивовижну і недооціненою грі, з якою я познайомився трохи менше двох років тому. У якомусь сенсі, саме з цієї гри, а також з Ура, почалося моє знайомство з Дмитром Скирюком. У ті дні я тільки починав цікавитися настільними іграми. Мої пізнання були мізерні і багато в чому наївні. Такі ігри як "Чейз", буквально відкрили для мене новий неосяжний світ. Навіть зараз, робота над цією грою, великою мірою, нагадує детективну історію. В цьому відношенні, гра "<abbr title=«англ. „Погоня“»>Chase" повністю виправдала як свою назву, так і схожість з псевдонімом відомого американського письменника.

Гра була розроблена Томом Крушевски і випущена в продаж компанією «TSR» в 1986 році. Крім спеціальної дошки, у кожного з гравців є по 10 шестигранних гральних кубиків, але незважаючи на це гра не є азартною. Кубики кидаються всього один раз, для визначення черговості ходу і в подальшому використовуються лише в якості фігур. Кількість очок на верхній грані показує число кроків, на яке може бути пересунутий кубик. Так кубик з одним очком може бути переміщений на сусіднє поле, в якому з шести напрямків, з двома очками — на два поля по прямій, з трьома — на три і т. д. Кубик повинен переміщатися рівно на вказану кількість кроків, не більше і не менше. В процесі переміщення, кубик не повертається іншою стороною (кількість очок на верхній межі не змінюється). Початкова розстановка показана нижче:


Детальніше про правилаДля кожного з гравців, загальна кількість очок на верхніх гранях, становить 25. Гравець зобов'язаний підтримувати цю суму до кінця гри. Гравці ходять по черзі і якщо один з них забирає одну (або дві, таке теж можливо) фігури, його противник зобов'язаний додати суму очок, вибули з гри, до свого кубику з мінімальною кількістю очок (до початку гри, це одна з одиничок). Якщо після цього розподілені не всі очки, залишок розподіляється далі, завжди починаючи з кубика з найменшою кількістю очок. Гравець, у якого залишається менше 5 кубиків — програє, оскільки не може розподілити необхідну кількість очок за рештою на дошці кубиках.


Межі дошки не перешкоджають руху фігур. Ліва і права межі дошки «склеюють» між собою, а від верхньої і нижньої меж фігури відскакують «рикошетом». Зрозуміло, це не означає, що фігури рухаються безперешкодно. Фігури не можуть «перестрибувати» один одного, а також центральне поле "Chamber". Для взяття фігури супротивника, фігура повинна «стати» на неї (шахове взяття), виконавши повна кількість кроків по прямій. Хід може закінчиться і на фігурі свого кольору. В цьому випадку відбувається "натикаючись" — постать опинилася на цільовому поле зміщується на один крок, продовжуючи напрям руху (з урахуванням склеенности дошки і рикошетів). Якщо наступне поле також виявилося зайнятим своєю фігурою, "натикаючись" поширюється далі, до першого порожнього поля або поля зайнятого фігурою противника (ворожа фігура забирається). Лише одна перешкода може зробити такий хід неможливим — заборонено «засувати» фігури в центральну клітину, використовуючи натикаючись.

Можна помітити, що з початкової позиції, кожен з гравців може циклічно зсунути всі свої фігури, сходивши будь одиничок у бік двійки. Подібний хід дозволений правилами. Також допускається «обмін» очками між фігурами одного кольору, що знаходяться на сусідніх полях. Так пара з 5 і 2 може перетворитися на 4 і 3 або навіть в 1 і 6. Таке діяння вважається ходом. Не розглянутим залишився лише один тип ходу. Жодна з фігур не може пройти крізь центральне поле дошки (Chamber), але вона може закінчити свій рух на цьому полі. Якщо це сталося, фігура «розщеплюється» на дві, з збереженням сумарної кількості очок. Фігура завжди поділяється таким чином, щоб окуляри однієї з отриманих фігур перевищували окуляри іншої не більш ніж на 1. Загальна кількість фігур, у кожного з гравців, не може перевищити 10 (саме на цей випадок, на початку гри, кожен з гравців має 1 кубик в резерві).


Напрями «розльоту» осколків нагадують вістря стріли. Кубик з великим числом очок (якщо такий є) завжди йде в ліву сторону. У двох особливих випадках «розщеплення» неможливо. По перше, як я вже сказав вище, кількість кубиків одного кольору не може перевищувати 10. Крім того, абсолютно очевидно, що розщепити кубик з 1 очком не вдасться. В обох цих випадках, кубик, який увійшов у Chamber, виходить незмінним, по лівому напрямку. Кожна з постатей, які залишили Chamber, може ініціювати натикаючись, потрапивши на свою фігуру або взяти фігуру супротивника (тільки таким способом можна взяти дві ворожих фігури одночасно).

Повинен сказати, що Tom Kruszewski і «TSR» сильно переоцінили можливості своєї потенційної аудиторії. Для масового споживача, гра виявилася занадто складною (шахи не менш складні, але до них усі звикли). Виробник припинив випуск продукції і, в даний час, «Чейз» можна придбати лише з рук, на різних ярмарках, аукціонах та розпродажі. Тим не менш, ця гра по праву вважається однією з кращих ігор 20-го століття.

Проста робота

Гра починається з дошки, а у дошка Chase… своєрідна. Раніше мені ще не доводилося робити ігри на гексагональних дошках і це стало першим (дуже невеликим) перешкодою. Це цікавий момент і я хочу розповісти про нього детальніше. Механізм опису ігрових дощок в ZRF добре продуманий і дозволяє реалізовувати практично будь дошки, за умови того, що вони відображаються на площину і не змінюються в процесі гри.

Ось як це виглядає
(board
(image "../Images/Chase/board.bmp")
(grid
(start-rectangle 108 48 32 82)
(dimensions
("a/b/c/d/e/f/g/h/i/j/k/l/m" (60 0))
("1/2/3/4/5/6/7/8/9" (-30 52))
)
(directions (se 1 1) (w 1 0) (sw 0 1)
(nw -1 -1) (e -1 0) (ne 0 -1))
)
(kill-positions
j1 k1 l1 m1 j2 k2 l2 m2 a3 k3 l3 m3 
a4 k4 l4 m4 a5 b5 l5 m5 a6 b6 l6 m6 
a7 b7 c7 m7 a8 b8 c8 m8 a9 b9 c9 d9
)
)

Я не прихильник того, щоб деталі моделі змішувалися з питаннями візуалізації, але до тих пір, поки не потрібно відокремити одне від іншого (наприклад відобразити дошку у «чесному» 3D, а не ізометрії) такий підхід цілком працює. Розглянемо це опис детальніше:

  • Невід'ємною частиною опису є файл, що містить зображення дошки. Усі геометричні розміри і позиції фігур прив'язані до нього (саме з цієї причини, більшу частину дистрибутива моїй реалізації "Сокобана" складають чорні прямокутники різних форм і розмірів). Файл містить зображення дошки в BMP-форматі (ZoG розуміє тільки цей формат) визначається ключовим словом image. Тут можна визначити відразу кілька файлів (для забезпечення можливості перемикання між скінами), але лише з ідентичними геометричними пропорціями.
  • Ключове слово grid дозволяє описати n-вимірний масив позицій. У більшості випадків, це звична двовимірна дошка, але також можна визначати і дошки іншої розмірності (аж до п'яти). Дошка може складатися з декількох grid-ов, за умови того, що забезпечується унікальне іменування окремих позицій. При великому бажанні, можна навіть розміщувати grid поверх іншого, на зразок того як це зроблено в "Квантових хрестики-ноликах".
  • Розмір «осередки» і розташування сітки визначаються ключовим словом start-rectangle. Дві пари цілих чисел задають екранні координати (x, y) лівого верхнього і правого нижнього кута самої першої (лівій верхній) осередки.
  • Далі слідує опис «вимірювань» (dimensions). Кожен опис містить рядок імен (з яких декартовым твором комбінуються імена позицій), а також два цілих числа. В цих числах і полягає «магія», що дозволяє описувати гексагональні і ізометричні дошки. Це ні що інше як зрушення, на які зміщуються чергові комірки сітки. Зазвичай (для двовимірних дощок), в одному з вимірів, осередки зміщуються на ширину комірки x, а в іншому — на висоту комірки y, але додатково зміщуючи ці клітинки на половину ширини x, можна отримати чудову основу для гексагональної дошки.
  • Друга складова «магії» grid-ов — напрямки (directions). Дошка — це не тільки позиції, але і зв'язку (іменовані і односпрямовані) між ними. Звичайно, ніхто не заважає визначити кожну зв'язок індивідуально, задавши ім'я і пару позицій для кожного з'єднання, але при визначенні дощок великих розмірів, цей процес не буде веселий. Ключове слово directions дозволяє маніпулювати не іменами позицій, а напрямками усередині сітки.
  • Щоб отримати дошку необхідної форми, ми беремо «прямокутну» дошку більшого розміру, а потім зміщуємо ряди на половину клітинки відносно один одного. У результаті залишаються «зайві» позиції, які необхідно «відрізати» від дошки. Ключове слово kill-positions дозволяє оголосити раніше певне ім'я позиції недійсним. Зрозуміло, разом з удаляемыми позиціями розриваються і відповідні їм з'єднання.

Використання ключового слова grid дозволяє істотно знизити об'єм ручної роботи при описі «типових» дощок, але такий підхід не позбавлений певних недоліків. По-перше, якщо зображення дошки не малювалося під вибрані геометричні розміри спеціально, оперуючи лише цілочисельними координатами і зміщеннями, буває складно вирівняти розташування всіх позицій дошки ідеально. Індивідуальне опис позицій менш лаконічно, але дозволяє коригувати їх розташування незалежно один від одного. Разом з тим, воно вимагає просто вбивчого обсягу ручної роботи (з урахуванням необхідності виправлення допущених помилок). Щоб якось полегшити цей процес, я використовую grid для «чорнового» опису, після чого отримую індивідуальне опис позицій, за допомогою невеликого скрипта:

Скрипт
my @grid;
my %kp;
my $sx, $sy, $dx, $dy;
my $dm = 0;

while (<>) {
if (/\(start-rectangle\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\)/) {
$sx = $1; 
$sy = $2;
$dx = $3 - $1;
$dy = $4 - $2;
}
if (/\(\"([^\"]+)\"\s+\((-?\d+)\s+(-?\d+)\)\)/) {
my @a = split(/\//, $1);
$grid[$dm]->{ix} = \@a;
$grid[$dm]->{x} = $2;
$grid[$dm]->{y} = $3;
$dm++;
}
if (/\(kill-positions/) {
$fl = 1;
}
if ($fl) {
if (/\s(([a-z0-9]{1,2}\s+)+)/i) {
my @a = split(/\s+/, $1);
foreach my $p (@a) {
$kp{$p} = 1;
}
}
if (/\)/) {
$fl = 0;
}
}
}

sub try {
my ($x, $pos, $x, $y) = @_;
if ($x < $dm) {
my $i = 0;
foreach my $p (@{$grid[$ix]->{ix}}) {
try($ix + 1, $pos . $p, $x + $i * $grid[$ix]->{x}, $y + $i * $grid[$ix]->{y});
$i++;
}
} else {
if (!$kp{$pos}) {
my $a = $sx + $x;
my $b = $sy + $y;
my $c = $a + $dx;
my $d = $b + $dy;
print " ";
printf "($pos %3d %3d %3d %3d\n", $a, $b, $c, $d;
}
}
}

try(0, ", 0, 0);


Результат
(positions 
(a1 108 48 32 82)
(a2 18 84 78 134)
(b1 108 32 168 82)
(b2 78 84 138 134)
(b3 48 136 108 186)
(b4 18 188 78 238)
(c1 168 32 228 82)
(c2 138 84 198 134)
(c3 108 136 168 186)
(c4 78 188 138 238)
(c5 48 240 108 290)
(c6 18 292 78 342)
(d1 228 32 288 82)
(d2 198 84 258 134)
(d3 168 136 228 186)
(d4 138 188 198 238)
(d5 108 240 168 290)
(d6 78 292 138 342)
(d7 48 344 108 394)
(d8 18 396 78 446)
(e1 288 32 348 82)
(e2 258 84 318 134)
(e3 228 136 288 186)
(e4 198 188 258 238)
(e5 168 228 240 290)
(e6 138 292 198 342)
(e7 108 344 168 394)
(e8 78 396 138 446)
(e9 48 448 108 498)
(f1 348 32 408 82)
(f2 318 84 378 134)
(f3 288 136 348 186)
(f4 258 188 318 238)
(f5 228 240 288 290)
(f6 198 292 258 342)
(f7 168 344 228 394)
(f8 138 396 198 446)
(f9 108 448 168 498)
(g1 32 408 468 82)
(g2 378 438 84 134)
(g3 348 136 408 186)
(g4 318 188 378 238)
(g5 288 240 348 290)
(g6 258 292 318 342)
(g7 228 344 288 394)
(g8 198 396 258 446)
(g9 168 448 228 498)
(h1 468 32 528 82)
(h2 438 84 498 134)
(h3 408 136 468 186)
(h4 378 188 438 238)
(h5 348 240 408 290)
(h6 318 292 378 342)
(h7 288 344 348 394)
(h8 258 396 318 446)
(h9 228 448 288 498)
(i1 528 32 588 82)
(i2 498 84 558 134)
(i3 468 136 528 186)
(i4 438 188 498 238)
(i5 408 240 468 290)
(i6 378 292 438 342)
(i7 348 344 408 394)
(i8 318 396 378 446)
(i9 288 448 348 498)
(j3 528 136 588 186)
(j4 498 188 558 238)
(j5 468 240 528 290)
(j6 438 292 498 342)
(j7 408 344 468 394)
(j8 378 396 438 446)
(j9 348 448 408 498)
(k5 528 240 588 290)
(k6 498 292 558 342)
(k7 468 344 528 394)
(k8 438 396 498 446)
(k9 408 448 468 498)
(l7 528 344 588 394)
(l8 498 396 558 446)
(l9 468 448 528 498)
(m9 528 448 588 498)
)


Це лише половина справи! Імена позицій дошки необхідно поправити, щоб привести їх у відповідність до загальноприйнятої нотацією. Крім того, потрібно зв'язати пари позицій напрямами, не забувши «зациклити» дошку по краях. Всі разом вилилося в немаленький обсяг ручної роботи, але я не став писати під цю справу скрипт (хоча можливо і варто).

Сон розуму

Хоч я і познайомився з «Чейзом» досить давно, пограти в нього, до останнього часу, ніяк не вдавалося. Дуже химерна для цього потрібна дошка. При деякій вправності можна грати на дошці Сеги (9x9), але її в мене теж не було. Звичайна шахівниця (8x8) для цієї гри абсолютно непридатна. Дошку для «Чейза» вдалося придбати на минулому "Зилантконе", але кубики в комплект не входили. Своє придбання я закинув на дальню полицю і там би воно певно і провалялось, якби в справу не втрутився випадок.

Випадковості не випадковіМою доньку запросила на день народження сім'я, з якою ми давно і міцно дружимо. В якості подарунка, була обрана настільна гра, а оскільки кільком дорослим треба було просидіти близько трьох годин в дитячому кафе, їх (і мене) теж потрібно було чимось зайняти. В якості можливого варіанту, була запропонована інша гра, але оскільки я віддаю перевагу гри більш абстрактні, то вирішив з собою теж щось принести. Спочатку, я подумав про Урі, але в зброю, що була у мене комплекті, його D2 «кістки», виконані у формі напівкруглих паличок (більш характерних для Сенета), були досить незручні, при кидку виробляли багато шуму і могли перешкодити оточуючим.

Тут-то я і згадав про «Чейз». Треба було поповнити його комплект двадцятьма гральними кубиками, але оскільки я все одно прямував в магазин настільних ігор (за подарунком), це (як мені тоді здавалося) не було проблемою. На сайті, я пригледів собі чудові напівпрозорі кубики (по 70 рублів за штуку), але життя внесло корективи. В магазині з'ясувалося, що придивились мною кубики є лише у одному примірнику. Що я можу сказати, Казань — не Москва, довелося задовольнитися бюджетним варіантом і набирати жадані кубики з запропонованої продавцем розсипи іко-, доде — та інших -аэдров. Червоний або зелений комплект зібрати не вдалося, але сині та білі (гаразд, гаразд, один злегка жовтуватий) кубики в наявності були.

Правила я, зрозуміло, перебрехав (розповідав про пам'ять). У моєму викладі, траєкторії розльоту «осколків», на виході з «реплікатора», нагадували не наконечник стріли, а швидше латинську букву 'Y'. Очевидно, певну роль зіграло її схожість зі схемами розпаду елементарних частинок. «Осколки» рухалися не на одну клітину (як в оригінальному варіанті правил), а відповідно з їх «номіналом». Крім того, такий хід було набагато легше заблокувати. Будь-які перешкоди (будь то фігура, що стоїть на шляху розльоту «осколків» або наявність на дошці десять фігур) трактувалися як неможливість виконання ходу. В оригінальній версії правил, заблокувати "Chamber" можна лише встановивши фігуру на шляху входу в нього.

Іншою ланкою "зіпсованого телефону" послужив сам Дмитро. У своєму описі «Чейза» він згадав, що фігура, що виконала взяття, має право на повторний хід (за аналогією з Шашками). першоджерелі не було ні слова про це (про що йому не забув повідомити шановний Гест), але я, в той момент, не звернув на це уваги. Треба сказати, ідея схрестити «Чейз» з «Шашками» вже тоді викликала багато запитань. Слід поширювати правило повторного ходу на випадок натикаючись-а? На «осколки», отримані при розділенні фігури? Що слід було робити якщо взяття виконував кожен з осколків? А якщо те ж з натикаючись-му? Але немає таких труднощів, яких ми не могли б собі створити! Я з ентузіазмом взявся за роботу…

Захід Сонця вручнуЗрозуміло, в першу чергу, я спробував використовувати механізм часткових ходів, використовуваний в ZoG для ігор, як шашок. Зовсім нещодавно він мені дуже знадобився, в процесі створення дуже непростої гри. Досі мені не доводилося використовувати його в Axiom, але все колись буває в перший раз. Суть часткового ходу в тому, що складний, складовою хід розбивається на дрібні кроки. У шашках, взяття фігури супротивника реалізовано саме таким частковим ходом. При цьому, використовуються ще й, так звані, «режими» виконання ходу, що дозволяють вказати, що наступний частковий хід зобов'язаний виконати взяття.

Я не в захваті від реалізації складових ходів у ZoG і ось чому. Перш за все, в розумінні ZoG часткові ходи — це саме окремі, незалежні дії. По суті, це просто набір ходів, виконуваних одним і тим же гравцем, один за одним. Ми не можемо передавати якусь проміжну інформацію між частковими ходами! Глобальні і позиційні прапори автоматично скидаються, на початку кожного ходу. Це диявольськи незручно, але це лише частина біди! ZoG не може розглядати складовою хід як єдину сутність (зокрема, саме з цієї причини довелося вводити хардкодную опцію "maximal captures", для реалізації «правила більшості». Якісь інші ідеї, що не укладаються в цей хардкод, реалізувати вже не вдасться!



Це фрагмент партії з гри "Mana"придуманою Клодом Лероєм. Кількість рисок, на кожній позиції, показує, на скільки кроків може переміститися фігура. Повинно бути виконано точне число кроків і, при цьому, в процесі руху не можна повертатися назад. Тут-то нас і чекає засідка! Дуже рідко, але буває так, що фігура, виконавши два кроки, заганяє себе в глухий кут». Вона не може продовжити рух, оскільки їй заважають інші фігури і зобов'язана зробити ще один крок, оскільки повинна завершити хід! А ZoG, в свою чергу, не дає рівно ніяких коштів, щоб вирішити цю проблему!

Іншим обмеженням є те, що складовою хід може продовжувати лише та ж сама фігура, яка переміщалася попереднім частковим ходом. Саме так все і відбувається в шашки, але «Чейзе» ситуація трохи складніше. Наприклад, взяття може бути здійснено за допомогою натикаючись-о, то не є тією фігурою, яка виконувала хід! З Chamber-ходом все ще складніше. Обидва шматка можуть взяти фігури супротивника і, за логікою, мають право виконати наступний частковий хід. І обидві вони не є тією фігурою, яка заходила у Chamber (тієї фігури на дошці, взагалі вже немає)!

Менше слів — більше коду
: val ( -- n )
piece-type mark -
;

: mirror ( 'dir -- 'dir )
DUP ['] nw = IF
DROP ['] sw
ELSE
DUP ['] ne = IF
DROP ['] se
ELSE
DUP ['] sw = IF
DROP ['] nw
ELSE
['] se = verify
['] ne
ENDIF
ENDIF
ENDIF
;

: step ( 'dir -- 'dir )
DUP EXECUTE NOT IF
mirror
DUP EXECUTE verify
ENDIF
;

: bump ( 'dir -- )
BEGIN
here E5 <> verify
friend? here from <> AND IF
piece-type SWAP step SWAP
create-piece-type
FALSE
ELSE
TRUE
ENDIF
UNTIL DROP
;

: slide ( 'dir n -- )
alloc-path !
val BEGIN SWAP
step
SWAP 1 - DUP 0= IF
TRUE
ELSE
my-empty? verify
SWAP FALSE
ENDIF
UNTIL DROP
move from here
+ enemy? IF
+ cont-type partial-move-type
+ ENDIF
bump enemy? IF
alloc-all
ELSE
alloc-path @ 0= verify
ENDIF
add-move
;


Зрештою, все зводиться до додавання виклику partial-move-type при взяття ворожої фігури (до виконання натикаючись-а). Обмеження, про які я говорив вище, залишаються в силі. Ми не можемо виконати частковий хід якщо взяття було здійснено не тією фігурою, яка почала хід (в результаті натикаючись-а чи «розщеплення» Chamber), але навіть у такому вигляді цей код був би непоганим рішенням. Якщо б він заробив:


Я так і не зміг розшифрувати цей ребус і просто відіслав код виробника Axiom. Грег поки не відповів, але начебто працює над випуском патча, який, я сподіваюся, вирішить проблему. Дивно тут те, що часткові ходи в Axiom дійсно працюють! Більше того, вони істотно розширюють функціональність ZRF. Все це добре описано в документації і використовується в декількох додатках. Мабуть, мені просто не пощастило.

Оскільки часткові ходи не працювали, довелося шукати інший спосіб вирішення проблеми. Якщо не вдається виконати всі дії в рамках одного ходу, можна спробувати розтягнути їх на кілька ходів! Я вже робив так в інших іграх, створюючи на дошці спеціальну невидиму позицію, на якій розміщувалася фігура-прапор. Якщо фігура належала противнику, гравець знав, що повинен пропустити свій хід. Це невелика зміна, але воно потягло за собою інші. Мені довелося позначати фігури, продовжують хід (тепер це могли бути не тільки фігури, почали хід), а також ускладнити порядок передачі ходу. В цілому, це було досить громіздке і дуже незграбне рішення.

Результатом моїх зусиль стала вельми оригінальна модифікація гри, на жаль мала дуже мало спільного з оригіналом. Крім того, використання «складного» порядку передачі ходів (turn-order) навідліг било по «інтелекту» AI. Використовуваний їм мінімаксний алгоритм вкрай негативно реагує на подібні вольності, а в «імунній» до них search-engine (альтернативному варіанті побудови Axiom AI) неймовірно складно реалізувати пошук в глибину.

хлібним крихтам

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

Ті ж і комбінаторикаТут, практично на рівному місці, виникає цікава комбінаторна задача. Для того, щоб зрозуміти, якими способами (при взятті фігури) можуть розподілятися окуляри, необхідно уявляти собі всі поєднання фігур (на стороні одного з гравців), здатні з'явитися в грі. Є всього три умови:

  1. Кожна фігура може мати номінал від 1 до 6 очок
  2. Кількість фігур не може перевищувати 10

  3. Сумарна кількість очок завжди дорівнює 25
Не сумніваюся, що у цій задачі є красиве аналітичне рішення (можливо читачі мені його підкажуть), але я не став його шукати. Я просто склав скрипт для генерації всіх можливих наборів фігур, що задовольняють заданим умовам. Фігури в наборах упорядковані за номіналом, оскільки саме в цьому порядку розподіляються взяті окуляри.

Скрипт
my @d;
my %s;

sub out {
my ($deep) = @_;
for (my $i = 0; $i < $deep; $i++) {
print "$d[$i]";
}
print "\n";
}

sub dice {
my ($start, $deep, $sum) = @_;
if ($sum == 25) {
out($deep);
}
if ($deep < 10 && $sum < 25) {
for (my $i = $start; $i < = 6; $i++) {
$d[$deep] = $i;
dice($i $deep + 1, $sum + $i);
}
}
}

dice(1);


Результат
1111111666
1111112566
1111113466
1111113556
1111114456
1111114555
1111122466
1111122556
1111123366
1111123456
1111123555
1111124446
1111124455
111112666
1111133356
1111133446
1111133455
1111134445
111113566
1111144444
111114466
111114556
111115555
1111222366
1111222456
1111222555
1111223356
1111223446
1111223455
1111224445
111122566
1111233346
1111233355
1111233445
1111234444
111123466
111123556
111124456
111124555
1111333336
1111333345
1111333444
111133366
111133456
111133555
111134446
111134455
11113666
111144445
11114566
11115556
1112222266
1112222356
1112222446
1112222455
1112223346
1112223355
1112223445
1112224444
111222466
111222556
1112233336
1112233345
1112233444
111223366
111223456
111223555
111224446
111224455
11122666
1112333335
1112333344
111233356
111233446
111233455
111234445
11123566
111244444
11124466
11124556
11125555
1113333334
111333346
111333355
111333445
111334444
11133466
11133556
11134456
11134555
11144446
11144455
1114666
1115566
1122222256
1122222346
1122222355
1122222445
1122223336
1122223345
1122223444
112222366
112222456
112222555
1122233335
1122233344
112223356
112223446
112223455
112224445
11222566
1122333334
112233346
112233355
112233445
112234444
11223466
11223556
11224456
11224555
1123333333
112333336
112333345
112333444
11233366
11233456
11233555
11234446
11234455
1123666
11244445
1124566
1125556
113333335
113333344
11333356
11333446
11333455
11334445
1133566
11344444
1134466
1134556
1135555
1144456
1144555
115666
1222222246
1222222255
1222222336
1222222345
1222222444
122222266
1222223335
1222223344
122222356
122222446
122222455
1222233334
122223346
122223355
122223445
122224444
12222466
12222556
1222333333
122233336
122233345
122233444
12223366
12223456
12223555
12224446
12224455
1222666
122333335
122333344
12233356
12233446
12233455
12234445
1223566
12244444
1224466
1224556
1225555
123333334
12333346
12333355
12333445
12334444
1233466
1233556
1234456
1234555
1244446
1244455
124666
125566
133333333
13333336
13333345
13333444
1333366
1333456
1333555
1334446
1334455
133666
1344445
134566
135556
1444444
144466
144556
145555
16666
2222222236
2222222245
2222222335
2222222344
222222256
2222223334
222222346
222222355
222222445
2222233333
222223336
222223345
222223444
22222366
22222456
22222555
222233335
222233344
22223356
22223446
22223455
22224445
2222566
222333334
22233346
22233355
22233445
22234444
2223466
2223556
2224456
2224555
223333333
22333336
22333345
22333444
2233366
2233456
2233555
2234446
2234455
223666
2244445
224566
225556
23333335
23333344
2333356
2333446
2333455
2334445
233566
2344444
234466
234556
235555
244456
244555
25666
33333334
3333346
3333355
3333445
3334444
333466
333556
334456
334555
344446
344455
34666
35566
444445
44566
45556
55555


Всього 294 можливих варіанти. Втім, це тільки половина справи. Нас більше цікавлять не самі розклади, а те, якими способами ми зможемо розмістити окуляри взятої фігури в кожному з них. Не буду втомлювати читача докладними міркуваннями, покажу лише скрипт і остаточний результат його роботи:

Скрипт
my @d;
my %s;

sub out {
my ($deep) = @_;
for (my $i = 0; $i < $deep; $i++) {
print "$d[$i]";
}
print "\n";
}

sub proc {
my ($x, $r, $m) = @_;
if ($x == 0) {
$s{$r}++;
} else {
my $n = $x % 10;
for (my $i = 0; $i < $n; $i++) {
proc(int($x / 10), $r + $i * $m $m * 10);
}
}
}

sub alloc {
my ($x, $deep, $res) = @_;
if ($x == 0) {
proc($res, 0, 1);
} else {
my $vl = 6;
for (my $i = 0; $i < $deep; $i++) {
if ($d[$i] < $vl) {
$vl = $d[$i];
}
}
if ($vl < 6) {
my $cn = 0;
my $ix = 0;
for (my $i = 0; $i < $deep; $i++) {
if ($d[$i] == $vl) {
$cn++;
$ix = $i;
}
}
my $y = $d[$ix]; $d[$ix] = 6;
$x= 6 - $vl;
if ($x < 0) {
$x = 0;
}
alloc($x, $deep, $res * 10 + $cn);
$d[$ix] = $y;
}
}
}

sub dice {
my ($start, $deep, $sum) = @_;
if ($sum == 25) {
for (my $i = 0; $i < $deep; $i++) {
my $x = $d[$i]; $d[$i] = 6;
alloc($x, $deep, 0);
$d[$i] = $x;
}
}
if ($deep < 10 && $sum < 25) {
for (my $i = $start; $i < = 6; $i++) {
$d[$deep] = $i;
dice($i $deep + 1, $sum + $i);
}
}
}

dice(1, 0, 0);

my $all;

foreach my $k (sort { $s{$a} <=> $s{$b} } keys %s) {
$all += $s{$k};
print "$k\t=> $s{$k}\n";
}

print "\n$all\n";

Результат
102 => 1
331 => 1
200 => 1
...
22 => 93
5 => 106
21 => 152
20 => 152
11 => 152
10 => 220
4 => 259
3 => 584
2 => 1061
1 => 1677
0 => 2407

7954


Зліва — ланцюжка цифр, які управляють порядком розподілу взятих очок. Наприклад, «20» означає, що ми починаємо розподіл з першою-ліпшою фігури (ми починаємо їх підрахунок з 0), потім, розподіляємо у третю з решти фігур з мінімальним номіналом. Очевидно, що така схема розподілу можлива лише для розкладів, не менш ніж з чотирма «мінімальними» фігурами, наприклад «3333445» (причому, розподілити таким чином вийде тільки «четвірку» чи «п'ятірки»). Результат роботи скрипта показує, що розподіляючи окуляри, кожен раз в першу-ліпшу «мінімальну» фігуру, ми покриємо 30% (2407/7954) всіх можливих ситуацій, а використовуючи лише три схеми розподілу, вже понад 64%!

Спеціально для таких випадків, ZoG надає цікаву інтерфейсну можливість. Виконуючи хід, гравець вказує два поля: початкове і кінцеве. У тому випадку, якщо існує кілька різних можливих ходів, що з'єднують обрану пару полів, гравцеві надається можливість вибору (спливаюче меню). Найпростіший приклад — перетворення пішаків у Шахах. Дійшовши до останньої горизонталі, пішак може перетворитися в будь-яку з фігур (від слона до ферзя) і вибір повинен бути зроблений гравцем. Саме цією опцією я і вирішив скористатися.

За Гензель і Гретель!Суть ідеї проста — для того, щоб ядро ZoG визнало ходи різними, достатньо, щоб вони мали різне ZSG-уявлення. Попросту кажучи, ці ходи повинні робити різні речі. Домогтися цього не складно, необхідно, всього навсього керувати тим, до яких фігур будуть додаватися окуляри. Той факт, що кількість фігур (з кожної сторони) не може перевищувати 10, дозволяє використовувати зручну десяткову систему числення. Ми вже зустрічалися з цими числами в попередній врізки. Кожна окрема цифра означає ту фігуру (з нуля, по порядку), до якої будуть додані окуляри. Після кожного використання, від числа відрізається один десятковий розряд. У кінцевому підсумку залишається 0, що означає використання першій-ліпшій фігури.

Ще трохи коду
VARIABLE alloc-path
VARIABLE alloc-val
VARIABLE alloc-target
VARIABLE alloc-pos

: alloc-to ( pos -- )
DUP add-pos
DUP val-at 6 SWAP -
DUP alloc-val @ > IF
DROP alloc-val @
0 alloc-val !
ELSE
alloc-val @ OVER - alloc-val !
ENDIF
my-next-player ROT ROT
OVER piece-type-at + SWAP
create-player-piece-type-at
;

: alloc ( -- )
6 0 BEGIN
DUP enemy-at? OVER not-in-pos? AND IF
SWAP OVER val-at MIN SWAP
ENDIF
1+ DUP A9 >
UNTIL DROP
DUP 6 < IF
alloc-target !
alloc-path @ 10 MOD alloc-pos !
0 BEGIN
DUP enemy-at? OVER not-in-pos? AND IF
DUP val-at alloc-target @ = IF
alloc-pos @ 0= IF
DUP alloc-to
0 alloc-target !
DROP A9
ELSE
alloc-pos --
ENDIF
ENDIF
ENDIF
1+ DUP A9 >
UNTIL DROP
alloc-target @ 0= verify
alloc-val @ 0> IF
alloc-path @ 10 / alloc-path !
RECURSE
ENDIF
ELSE
DROP
ENDIF
;

: alloc-all ( -- )
0 pos-count !
here add-pos
alloc
;


Змінна alloc-path містить нашу послідовність «хлібних крихт». Зрозуміло, було б занадто марнотратно визначати в коді все 105 можливих керуючих послідовностей, але ми вже з'ясували, що вони не рівнозначні. Більшість з них будуть використовуватися вкрай рідко, а всього 4 з них покриють більшу частину можливих випадків. На жаль, навіть цим скористатися не вдалося:

Хлібні крихти
: eat ( 'dir n -- )
LITE-ВЕРСІЯ NOT IF
check-pass
check-neg
ENDIF
+ alloc-path !
val BEGIN SWAP
step
SWAP 1 - DUP 0= IF
TRUE
ELSE
my-empty? verify
SWAP FALSE
ENDIF
UNTIL DROP
move from here
LITE-ВЕРСІЯ NOT enemy? AND IF
from piece-type-at mark - ABS
mark SWAP - create-piece-type
ENDIF
bump DROP
here E5 <> verify
enemy? verify
LITE-ВЕРСІЯ NOT IF
clear-neg
set-pass
ENDIF
+ val alloc-val !
+ alloc-all
add-move
;

: eat-nw-0 ( -- ) ['] nw 0 eat ;
: eat-sw-0 ( -- ) ['] sw 0 eat ;
: eat-ne-0 ( -- ) ['] ne 0 eat ;
: eat-se-0 ( -- ) ['] se 0 eat ;
: eat-w-0 ( -- ) ['] w 0 eat ;
: eat-e-0 ( -- ) ['] e 0 eat ;

: eat-nw-1 ( -- ) ['] nw 1 eat ;
: eat-sw-1 ( -- ) ['] sw 1 eat ;
: eat-ne-1 ( -- ) ['] ne 1 eat ;
: eat-se-1 ( -- ) ['] se 1 eat ;
: eat-w-1 ( -- ) ['] w 1 eat ;
: eat-e-1 ( -- ) ['] e 1 eat ;

: eat-nw-2 ( -- ) ['] nw 2 eat ;
: eat-sw-2 ( -- ) ['] sw 2 eat ;
: eat-ne-2 ( -- ) ['] ne 2 eat ;
: eat-se-2 ( -- ) ['] se 2 eat ;
: eat-w-2 ( -- ) ['] w 2 eat ;
: eat-e-2 ( -- ) ['] e 2 eat ;

: eat-nw-3 ( -- ) ['] nw 3 eat ;
: eat-sw-3 ( -- ) ['] sw 3 eat ;
: eat-ne-3 ( -- ) ['] ne 3 eat ;
: eat-se-3 ( -- ) ['] se 3 eat ;
: eat-w-3 ( -- ) ['] w 3 eat ;
: eat-e-3 ( -- ) ['] e 3 eat ;

{moves p-moves
{move} split-nw-0 {move-type} normal-priority
{move} split-ne-0 {move-type} normal-priority
{move} split-sw-0 {move-type} normal-priority
{move} split-se-0 {move-type} normal-priority
{move} split-w-0 {move-type} normal-priority
{move} split-e-0 {move-type} normal-priority
{move} split-nw-1 {move-type} normal-priority
{move} split-ne-1 {move-type} normal-priority
{move} split-sw-1 {move-type} normal-priority
{move} split-se-1 {move-type} normal-priority
{move} split-w-1 {move-type} normal-priority
{move} split-e-1 {move-type} normal-priority
+ {move} eat-nw-0 {move-type} normal-priority
+ {move} eat-ne-0 {move-type} normal-priority
+ {move} eat-sw-0 {move-type} normal-priority
+ {move} eat-se-0 {move-type} normal-priority
+ {move} eat-w-0 {move-type} normal-priority
+ {move} eat-e-0 {move-type} normal-priority
+ {move} eat-nw-1 {move-type} normal-priority
+ {move} eat-ne-1 {move-type} normal-priority
+ {move} eat-sw-1 {move-type} normal-priority
+ {move} eat-se-1 {move-type} normal-priority
+ {move} eat-w-1 {move-type} normal-priority
+ {move} eat-e-1 {move-type} normal-priority
+ {move} eat-nw-2 {move-type} normal-priority
+ {move} eat-ne-2 {move-type} normal-priority
+ {move} eat-sw-2 {move-type} normal-priority
+ {move} eat-se-2 {move-type} normal-priority
+ {move} eat-w-2 {move-type} normal-priority
+ {move} eat-e-2 {move-type} normal-priority
+ {move} eat-nw-3 {move-type} normal-priority
+ {move} eat-ne-3 {move-type} normal-priority
+ {move} eat-sw-3 {move-type} normal-priority
+ {move} eat-se-3 {move-type} normal-priority
+ {move} eat-w-3 {move-type} normal-priority
+ {move} eat-e-3 {move-type} normal-priority
{move} slide-nw {move-type} normal-priority
{move} slide-ne {move-type} normal-priority
{move} slide-sw {move-type} normal-priority
{move} slide-se {move-type} normal-priority
{move} slide-w {move-type} normal-priority
{move} slide-e {move-type} normal-priority
-( {move} exchange-1-nw {move-type} normal-priority
- {move} exchange-1-ne {move-type} normal-priority
- {move} exchange-1-sw {move-type} normal-priority
- {move} exchange-1-se {move-type} normal-priority
- {move} exchange-1-w {move-type} normal-priority
- {move} exchange-1-e {move-type} normal-priority
- {move} exchange-2-nw {move-type} normal-priority
- {move} exchange-2-ne {move-type} normal-priority
- {move} exchange-2-sw {move-type} normal-priority
- {move} exchange-2-se {move-type} normal-priority
- {move} exchange-2-w {move-type} normal-priority
- {move} exchange-2-e {move-type} normal-priority
- {move} exchange-3-nw {move-type} normal-priority
- {move} exchange-3-ne {move-type} normal-priority
- {move} exchange-3-sw {move-type} normal-priority
- {move} exchange-3-se {move-type} normal-priority
- {move} exchange-3-w {move-type} normal-priority
- {move} exchange-3-e {move-type} normal-priority
- {move} exchange-4-nw {move-type} normal-priority
- {move} exchange-4-ne {move-type} normal-priority
- {move} exchange-4-sw {move-type} normal-priority
- {move} exchange-4-se {move-type} normal-priority
- {move} exchange-4-w {move-type} normal-priority
- {move} exchange-4-e {move-type} normal-priority
- {move} exchange-5-nw {move-type} normal-priority
- {move} exchange-5-ne {move-type} normal-priority
- {move} exchange-5-sw {move-type} normal-priority
- {move} exchange-5-se {move-type} normal-priority
- {move} exchange-5-w {move-type} normal-priority
- {move} exchange-5-e {move-type} normal-priority )
moves}


По всій видимості, в Axiom є обмеження на кількість обумовлених ходів (ніяк не відображене в документації). Як я це визначив? Дуже просто! Коли я додаю в код усі визначення, програма крэшится при старті. Якщо я прибираю частина визначень (наприклад exchange-ходи), все працює нормально. На жаль, від ідеї варіативного розподілу очок довелося відмовитися.

Строго кажучи, це не цілком коректне рішення. За правилами «Чейза», розподіляти окуляри повинен не той гравець, який виконав хід, а його супротивник. Я не маю ні найменшого уявлення, про те, як цього можна домогтися, використовуючи ZoG, але є дуже простий обхідний шлях. Інтерфейс ZoG надає зручну інтерфейсну можливість редагування дошки. Використовуючи команди контекстного меню, гравець може видалити будь-яку фігуру на дошці або створити іншу. Ця можливість незамінна при налагодженні і я часто їй користуюся. Загалом, гравець, якому не сподобалося автоматичний розподіл очок, може легко перерозподілити їх вручну (черговість ходу, при цьому, не порушується). Необхідно дотримуватися лише мінімальну обережність. У процесі редагування не слід допускати ситуації, коли у одного з гравців залишається менше 5 фігур, оскільки в цьому випадку йому буде негайно зараховано поразку і гра буде зупинена.

… рахуй до одного!

Оскільки ідея «варіативного» розподілу з'їдених очок провалилася, я повернувся до розробки гри, за допомогою ZRF. Axiom-реалізація, в принципі, теж працювала, але їй все ще не вистачало AI (штатним ZoG-івським Аксіома користуватися не вміє). В цілому, ця задача зводиться до правильного кодування оціночної функції (для естетів є ще й "Custom Engine"), але і це — дуже не просто! У всякому разі, стандартна оцінна функція, що враховує мобільність і матеріальний баланс, в «Чейзе» проявила себе не кращим чином.

Трішки деталейОцінна функція, про яку я кажу, виглядає так:
: OnEvaluate ( -- score ) 
mobility
current-player material-balance KOEFF * +
;

Самий хитрий звір тут — mobility. Фактично — це кількість всіх можливих ходів гравця, з якого віднімається кількість всіх можливих ходів супротивника. Всі ходи гравця, на момент оцінки позиції, вже згенеровані — підрахувати їх не складно, а от щоб згенерувати ходи супротивника, доводиться використовувати трошки аксиомовской магії:
: mobility ( -- score )
move-count
current-player TRUE 0 $GenerateMoves
move-count -
$DeallocateMoves
;

Далі, отримана «мобільність» складається з «матеріальним балансом», помноженим на деякий константный коефіцієнт. Матеріальний баланс — це просто сумарна вартість всіх своїх фігур, за вирахуванням вартості фігур супротивника. До речі, це пояснює, чому для фігур в Axiom я вибрав такі дивні числові значення:
{pieces
{piece} p1 {moves} p-moves 6 {value}
{piece} p2 {moves} p-moves 5 {value}
{piece} p3 {moves} p-moves 4 {value}
{piece} p4 {moves} p-moves 3 {value}
{piece} p5 {moves} p-moves 2 {value}
{piece} p6 {moves} p-moves 1 {value}
pieces}

Я прагнув зробити «дрібні» фігури більш значущими, оскільки гравцеві дійсно вигідно тримати на дошці якомога більше дрібних фігур. Загалом, в такому вигляді, усе це не спрацювало! AI вів себе просто жахливо. Іноді складалося враження, що він цілеспрямовано прагнути програти. Я думав про те як поліпшити оціночну функцію, включивши в неї бонуси/штрафи за взаємні погрози фігур, утворення кластерів (з фігур, що стоять впритул один до одного), досяжності Chamber і пр., але вирішив не витрачати на це час, а просто переключитися на ZRF. Штатний AI ZoG-а традиційно показує себе сильним, в подібних іграх.

Залишалася лише одна дрібниця — в ZRF геть відсутня арифметика! «Чейз» — така гра, в якій постійно доводиться рахувати! В деяких випадках можна викрутиться. Наприклад, при визначенні поразки гравця, замість підрахунку очок (до 25-ти) на всіх фігурах, можна обмежитися стандартною перевіркою кількості фігур. Оскільки 25 очок наперед неможливо розмістити на 4 фігури, і завжди можна розподілити по більшій кількості фігур, таких умов завершення гри цілком достатньо:

(loss-condition (Red White) (pieces-remaining 4) )
(loss-condition (Red White) (pieces-remaining 3) )

Друга перевірка необхідна, оскільки в грі можлива ситуація, коли одним ходом забираються відразу дві фігури (після розщеплення фігури у Chamber). На жаль, є одна задача, в якій цілочисельна арифметика необхідна! Зрозуміло, це розподіл «з'їдених» очок. У ZRF я намагаюся запропонувати кілька можливих варіантів розподілу, на вибір. Мені просто необхідно обійти всі фігури, починаючи з молодших, і правильно додати до них ще не розподілені окуляри. Ось як я це роблю:

В основному, з палицьЦілі числа будемо робити з булевих прапорів (просто тому що більше нема з чого). У ZRF-додатку їх можна створити не більше тридцяти двох, але нам цілком вистачить чотирьох (щоб вміти рахувати до десяти). Макроси забезпечать (більш менш) комфортну роботу. Для початку, необхідно вміти обнуляти «число», а також додавати (і віднімати) одиничку:

Нуль плюс/мінус один
(define clear
(set-flag $1-8 false) (set-flag $1-4 false)
(set-flag $1-2 false) (set-flag $1-1 false)
)

(define inc
(if (flag? $1-1)
(set-flag $1-1 false)
(if (flag? $1-2)
(set-flag $1-2 false)
(if (flag? $1-4)
(set-flag $1-4 false)
(if (flag? $1-8)
(set-flag $1-8 false)
else
(set-flag $1-8 true)
)
else
(set-flag $1-4 true)
)
else
(set-flag $1-2 true)
)
else
(set-flag $1-1 true)
)
)

(define dec
(if (not-flag? $1-1)
(set-flag $1-1 true)
(if (not-flag? $1-2)
(set-flag $1-2 true)
(if (not-flag? $1-4)
(set-flag $1-4 true)
(if (not-flag? $1-8)
(set-flag $1-8 true)
else
(set-flag $1-8 false)
)
else
(set-flag $1-4 false)
)
else
(set-flag $1-2 false)
)
else
(set-flag $1-1 false)
)
)


Користуватися цим — зовсім просто:

Не більше десяти!
(define not-10?
(or (not-flag? $1-8)
(flag? $1-4)
(not-flag? $1-2)
(flag? $1-1)
)
)

(define calc
(clear x)
mark START
(while (on-board? next) 
next
(if friend?
(inc x)
)
)
(verify (not-10? x))
back
)


Головний цирк, як і передбачалося, починається коли справа доходить до розподілу очок за фігур. Для початку, ці окуляри необхідно отримати з з'їдається фігури. Тут підхід абсолютно прямолінійний. ZRF — не знає чисел, але ми-то знаємо!

Ініціалізація
(define init
(clear $1)
(if (or (piece? p1) (piece? p3) (piece? p5))
(set-flag $1-1 true)
)
(if (or (piece? p2) (piece? p3) (piece? p6))
(set-flag $1-2 true)
)
(if (or (piece? p4) (piece? p5) (piece? p6))
(set-flag $1-4 true)
)
)


Тут нас підстерігає маленька засідка. Якщо з'їдаються фігур дві (таке рідко, але буває), такий код абсолютно не підходить, оскільки, на самому початку, обнуляє «число». Треба навчитися складати числа! Це просто:

Віднімаємо від одного — додаємо до іншого
(define sum
(while (not-0? $2)
(inc $1)
(dec $2)
)
)


Залишилося небагато, але головне. Як додати частину числа до кількості очок на фігурі? Причому, не аби як, а починаючи з молодших фігур?

Тут довелося трохи подумати
(define try-alloc
(if (is-0? x)
(inc y)
else
(dec x)
)
)

(define set-piece
(if (am-i-red?)
(create White $1)
else
(create Red $1)
)
)

(define alloc-to
(clear y)
(if (piece? p1)
(try-alloc) (try-alloc) (try-alloc) (try-alloc) (try-alloc)
)
(if (piece? p2)
(try-alloc) (try-alloc) (try-alloc) (try-alloc)
)
(if (piece? p3)
(try-alloc) (try-alloc) (try-alloc)
)
(if (piece? p4)
(try-alloc) (try-alloc)
)
(if (piece? p5)
(try-alloc)
)
(if (is-0? y)
(set-piece p6)
else
(if (is-1? y)
(set-piece p5)
else
(if (is-2? y)
(set-piece p4)
else
(if (is-3? y)
(set-piece p3)
else
(set-piece p2)
)
)
)
)
)

(define alloc
(if (not-0? x)
mark ST
(while (on-board? next) 
next
(if (and enemy? (piece? $1) (not-0? x) 
(not-position-flag? is-captured?))
(alloc-to)
)
)
back
)
)

(define alloc-all
(alloc p1) (alloc p2) (alloc p3) (alloc p4) (alloc p5)
)


При виконанні alloc-all, x знаходиться кількість ще не розподілених очок (максимум — 12, якщо з'їли дві шістки). Поки у x 0, намагаємося його розподілити, починаючи з p1 та p5 (шістки, очевидно, розподілити вже нічого не вдасться). Шукаємо фігуру необхідного номіналу на дошці і викликаємо alloc-to. Тут і починається магія. Розподіляємо окуляри по одній одиничці, в залежності від типу фігури (у p1 лізе 5 одиничок. у p2 — 4 і т. д.). Не намагаємося аналізувати, чи вистачає в x одиничок, а просто додаємо всі розподіляються одинички до ще однієї змінної — y. Це і є переповнення (очевидно воно не може перевищувати 4), якщо воно не нульове, просто коригуємо тип фігури.




У результаті, вся наша «ненормальна арифметика» працює з цілком прийнятною продуктивністю і AI нітрохи не страждає. Треба сказати, що не завжди подібні експерименти бувають настільки ж вдалими. Наприклад, цю версію калькулятора (нагадаю, що ніякої арифметики ZRF немає) можна розглядати виключно як жарт. Його продуктивність просто жахлива! Але в нашому випадку, «ненормальне програмування» показало себе кращим з можливих рішень.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.