- PATTERNS: авианосец, длинная лодка, каноэ, MWSS, HWSS, Loafer, кролики, ружьё Симкина (22 → 30) - CAMPAIGN_LEVELS: ручные «Флотилия»/«Погоня»/«Кролики» после «Издалека» (100 → 103) - README: раздел «Интересные факты» и список ресурсов - tests: счётчики и проверки новых фигур/уровней синхронизированы
3379 lines
251 KiB
HTML
3379 lines
251 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
||
<meta name="theme-color" content="#111418">
|
||
<title>Life — территориальная борьба</title>
|
||
<!-- Favicon — инлайн SVG (data:URI), как и всё остальное: без внешних ассетов.
|
||
Глайдер Конвея: зелёные клетки P2 и красные P1 на тёмной скруглённой плашке. -->
|
||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3E%3Crect%20width='32'%20height='32'%20rx='6'%20fill='%23111418'/%3E%3Crect%20x='12'%20y='4'%20width='8'%20height='8'%20fill='%232ecc71'/%3E%3Crect%20x='20'%20y='12'%20width='8'%20height='8'%20fill='%232ecc71'/%3E%3Crect%20x='4'%20y='20'%20width='8'%20height='8'%20fill='%23e74c3c'/%3E%3Crect%20x='12'%20y='20'%20width='8'%20height='8'%20fill='%23e74c3c'/%3E%3Crect%20x='20'%20y='20'%20width='8'%20height='8'%20fill='%23e74c3c'/%3E%3C/svg%3E">
|
||
<style>
|
||
/* ===================== БАЗОВЫЙ КАРКАС ===================== */
|
||
:root{
|
||
/* Нативные контролы (popup селектов, скроллбары, спиннеры) следуют теме. */
|
||
color-scheme:dark;
|
||
/* отступы под «чёлку»/жесты — safe-area для HUD и оверлеев */
|
||
--safe-t: env(safe-area-inset-top, 0px);
|
||
--safe-r: env(safe-area-inset-right, 0px);
|
||
--safe-b: env(safe-area-inset-bottom, 0px);
|
||
--safe-l: env(safe-area-inset-left, 0px);
|
||
--bg:#111418;
|
||
--fg:#e8e8e8;
|
||
--p1:#e74c3c; /* цвет игрока 1 */
|
||
--p2:#2ecc71; /* цвет игрока 2 */
|
||
--panel:#1b2026;
|
||
--line:rgba(255,255,255,.14);
|
||
--side-w:320px; /* ширина боковой панели управления в ландшафте (см. computeBoard) */
|
||
/* Структурные цвета UI (тема). Переопределяются в :root[data-theme="light"]. */
|
||
--chip-bg:rgba(20,24,30,.82); /* плашки HUD (метка/счёт/бюджет/кнопки) */
|
||
--chip-bg-active:rgba(40,48,58,.9); /* нажатое состояние кнопок HUD */
|
||
--chip-bg2:rgba(28,34,42,.92); /* фон кнопок действий .act */
|
||
--overlay-bg:rgba(8,10,13,.8); /* подложка модальных оверлеев */
|
||
--panel-bg:linear-gradient(165deg,#232a33,#161b21 60%,#12161a); /* панель диалогов */
|
||
--panel-fg:#eef2f6; /* основной текст панелей */
|
||
--muted:#b7c2ce; /* вторичный текст (описания, подписи) */
|
||
--opt-bg:rgba(255,255,255,.05); /* фон строк-опций меню */
|
||
--input-bg:rgba(0,0,0,.35); /* фон полей ввода/селектов */
|
||
--panel-inset:rgba(255,255,255,.08); /* верхний блик панели */
|
||
/* Активное состояние инструмента/штампа/скорости: фон + читаемый текст.
|
||
В светлой теме переопределяются (иначе тёмный фон + тёмный текст). */
|
||
--hl-bg:rgba(70,60,20,.9);
|
||
--hl-fg:#ffe9b0;
|
||
}
|
||
/* Светлая тема: переключается атрибутом data-theme на <html> (applyTheme).
|
||
Цвета игроков (--p1/--p2) одинаковы в обеих темах — различаемость клеток
|
||
не зависит от темы; меняется лишь фон/панели/текст. */
|
||
:root[data-theme="light"]{
|
||
color-scheme:light;
|
||
--bg:#eef1f5;
|
||
--fg:#1a1f26;
|
||
--line:rgba(0,0,0,.14);
|
||
--chip-bg:rgba(255,255,255,.88);
|
||
--chip-bg-active:rgba(0,0,0,.08);
|
||
--chip-bg2:rgba(255,255,255,.92);
|
||
--overlay-bg:rgba(238,241,245,.75);
|
||
--panel-bg:linear-gradient(165deg,#ffffff,#f2f4f7 60%,#e9edf2);
|
||
--panel-fg:#1a1f26;
|
||
--muted:#5a646f;
|
||
--opt-bg:rgba(0,0,0,.04);
|
||
--input-bg:rgba(0,0,0,.06);
|
||
--panel-inset:rgba(255,255,255,.7);
|
||
/* Активная подсветка: непрозрачное золото (может лежать над canvas) + тёмный текст. */
|
||
--hl-bg:#f3cf7d;
|
||
--hl-fg:#4a2e00;
|
||
}
|
||
*{box-sizing:border-box}
|
||
html,body{
|
||
margin:0;height:100%;overflow:hidden;background:var(--bg);color:var(--fg);
|
||
font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||
-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;
|
||
}
|
||
/* Игровое полотно — на весь экран; touch-action:none глушит скролл/зум жестами
|
||
(ввод — только Pointer Events, см. AGENTS.md). */
|
||
#game{position:fixed;inset:0;display:block;touch-action:none;background:var(--bg)}
|
||
button{font-family:inherit;cursor:pointer;color:inherit}
|
||
.hidden{display:none!important}
|
||
|
||
/* ===================== HUD ===================== */
|
||
#hud{
|
||
position:fixed;top:0;left:0;right:0;z-index:5;display:flex;align-items:center;
|
||
justify-content:space-between;gap:6px;pointer-events:none;
|
||
padding:calc(6px + var(--safe-t)) calc(8px + var(--safe-r)) 6px calc(8px + var(--safe-l));
|
||
}
|
||
#turnLabel{
|
||
pointer-events:auto;font-weight:700;font-size:15px;padding:9px 14px;border-radius:999px;
|
||
background:var(--chip-bg);border:1px solid var(--line);
|
||
box-shadow:0 3px 12px rgba(0,0,0,.4);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
#hudright{display:flex;align-items:center;gap:5px;pointer-events:auto;min-width:0}
|
||
#budgetLabel{
|
||
font-weight:700;font-size:12px;padding:5px 8px;border-radius:999px;
|
||
background:var(--chip-bg);border:1px solid var(--line);
|
||
box-shadow:0 3px 12px rgba(0,0,0,.4);white-space:nowrap;color:var(--fg);
|
||
min-width:0;overflow:hidden;text-overflow:ellipsis;
|
||
}
|
||
#budgetLabel.p1{color:var(--p1)}
|
||
#budgetLabel.p2{color:var(--p2)}
|
||
#budgetLabel.zero{color:#ff6b5e;border-color:rgba(231,76,60,.6)}
|
||
#scores{
|
||
display:flex;align-items:center;gap:4px;font-weight:800;font-size:14px;
|
||
padding:6px 9px;border-radius:999px;background:var(--chip-bg);
|
||
border:1px solid var(--line);box-shadow:0 3px 12px rgba(0,0,0,.4);
|
||
}
|
||
#scoreP1{color:var(--p1)}
|
||
#scoreP2{color:var(--p2)}
|
||
.scsep{opacity:.45}
|
||
#btnSettings{
|
||
width:40px;height:40px;border-radius:50%;font-size:17px;border:1px solid var(--line);
|
||
background:var(--chip-bg);box-shadow:0 3px 12px rgba(0,0,0,.4);transition:transform .12s,background .15s;
|
||
}
|
||
#btnSettings:active{transform:scale(.94);background:var(--chip-bg-active)}
|
||
/* Узкие экраны (320…400): сжимаем HUD, чтобы метка фазы НЕ обрезалась и кнопка
|
||
настроек не уходила за край. Порог 400px (а не 380) убирает микро-обрезку метки
|
||
в полосе 381–388px (minor ре-ревью R2-2). #hud не переносим — computeBoard
|
||
резервирует top:64px. */
|
||
@media (max-width:400px){
|
||
#hud{gap:4px;padding:calc(4px + var(--safe-t)) calc(5px + var(--safe-r)) 4px calc(5px + var(--safe-l))}
|
||
#hudright{gap:4px}
|
||
#turnLabel{font-size:12px;padding:5px 7px}
|
||
#budgetLabel{font-size:11px;padding:4px 5px}
|
||
#scores{font-size:12px;padding:5px 6px;gap:2px}
|
||
#btnSettings{width:40px;height:40px;font-size:16px}
|
||
}
|
||
|
||
/* ===================== НИЖНИЕ ПАНЕЛИ ===================== */
|
||
#toolbar{
|
||
position:fixed;left:0;right:0;z-index:5;display:flex;justify-content:center;gap:8px;
|
||
flex-wrap:wrap;row-gap:8px; /* с кнопками поворота/тапа инструменты переносятся, а не обрезаются */
|
||
bottom:calc(74px + var(--safe-b));padding:0 10px;
|
||
}
|
||
.toolbtn{
|
||
width:62px;height:56px;border-radius:16px;border:1px solid var(--line);
|
||
background:var(--chip-bg);display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||
gap:2px;font-size:20px;line-height:1;box-shadow:0 3px 12px rgba(0,0,0,.4);
|
||
transition:transform .12s,background .15s,border-color .15s;
|
||
}
|
||
.toolbtn span{font-size:10px;font-weight:700;opacity:.7}
|
||
.toolbtn:active{transform:scale(.95)}
|
||
.toolbtn.active{border-color:#ffd75e;background:var(--hl-bg);color:var(--hl-fg);box-shadow:0 0 0 2px rgba(255,215,94,.45),0 3px 12px rgba(0,0,0,.4)}
|
||
.toolbtn:disabled{opacity:.35}
|
||
|
||
/* Дропдаун готовых паттернов (штамп) в тулбаре: рядом с инструментами. */
|
||
#optPattern{
|
||
flex:0 1 auto;min-width:96px;width:auto;max-width:130px;height:56px;padding:0 8px;
|
||
border-radius:16px;border:1px solid var(--line);background:var(--chip-bg);color:var(--fg);
|
||
font-family:inherit;font-size:13px;font-weight:700;text-overflow:ellipsis;
|
||
box-shadow:0 3px 12px rgba(0,0,0,.4);transition:border-color .15s,background .15s;
|
||
}
|
||
#optPattern:disabled{opacity:.35}
|
||
#optPattern.active{border-color:#ffd75e;background:var(--hl-bg);color:var(--hl-fg);box-shadow:0 0 0 2px rgba(255,215,94,.45),0 3px 12px rgba(0,0,0,.4)}
|
||
|
||
/* Свой список паттернов (заменяет нативный popup <select>). Позиционируется в JS
|
||
рядом с #optPattern; на длинном списке не «накрывает» курсор, поэтому клик по
|
||
выбранной фигуре не срабатывает сам по себе. */
|
||
#patternMenu{
|
||
position:fixed;z-index:30;padding:6px;border-radius:14px;border:1px solid var(--line);
|
||
background:var(--panel-bg);color:var(--panel-fg);
|
||
box-shadow:0 18px 48px rgba(0,0,0,.55);
|
||
max-height:min(56vh,420px);overflow-y:auto;overscroll-behavior:contain;
|
||
-webkit-overflow-scrolling:touch;
|
||
}
|
||
#patternMenu .patgroup{
|
||
padding:8px 10px 3px;font-size:11px;font-weight:800;letter-spacing:.4px;
|
||
text-transform:uppercase;color:var(--muted);
|
||
}
|
||
#patternMenu .patitem{
|
||
display:block;width:100%;text-align:left;padding:9px 10px;border:0;border-radius:9px;
|
||
background:transparent;color:inherit;font:inherit;font-size:14px;font-weight:600;cursor:pointer;
|
||
transition:background .12s,color .12s;
|
||
}
|
||
#patternMenu .patitem:hover{background:var(--chip-bg2)}
|
||
#patternMenu .patitem.active{background:var(--hl-bg);color:var(--hl-fg)}
|
||
|
||
#actions{
|
||
position:fixed;left:0;right:0;z-index:5;display:flex;justify-content:center;gap:8px;
|
||
flex-wrap:wrap;row-gap:8px; /* пауза: до 4 кнопок инлайн — переносим, а не обрезаем */
|
||
bottom:calc(12px + var(--safe-b));padding:0 10px;
|
||
}
|
||
.act{
|
||
min-height:48px;padding:0 16px;border-radius:14px;border:1px solid var(--line);
|
||
background:var(--chip-bg2);font-weight:700;font-size:15px;white-space:nowrap;
|
||
box-shadow:0 3px 12px rgba(0,0,0,.4);transition:transform .12s,background .15s,filter .12s;
|
||
}
|
||
.act:active{transform:translateY(2px)}
|
||
.act.primary{background:linear-gradient(#ffd76e,#f5a623);color:#241a00;border-color:#ffd75e}
|
||
/* «Завершить партию» — нейтральная тёмно-серая (не красная): не необратима, а под вопросом. */
|
||
.act.danger{background:rgba(48,52,58,.95);border-color:var(--line);color:#fff}
|
||
.act.wide{width:100%;margin-top:10px}
|
||
/* «В меню» — тёмно-синяя: заметна на фоне прочих действий, но не «опасная». */
|
||
.act.menu{background:rgba(26,52,92,.95);border-color:rgba(70,130,200,.55);color:#fff}
|
||
/* Экран выбора уровня кампании: длинный список уровней — прокручиваемый, чтобы
|
||
заголовок панели и кнопка «В меню» оставались на виду. */
|
||
#levelList{max-height:min(58vh,460px);overflow-y:auto;overscroll-behavior:contain;
|
||
-webkit-overflow-scrolling:touch;display:flex;flex-direction:column;gap:8px;margin-top:10px}
|
||
#levelList .levelbtn{margin-top:0}
|
||
/* «Дорисовать» — кнопки конкретного игрока: слегка подсвечены его цветом. */
|
||
.act.editp1{border-color:rgba(231,76,60,.7);background:rgba(231,76,60,.18)}
|
||
.act.editp2{border-color:rgba(46,204,113,.7);background:rgba(46,204,113,.18)}
|
||
/* «Скорость» — ряд из 4 маленьких кнопок 1×/2×/4×/8×; активна выбранная. */
|
||
.speedrow{display:inline-flex;gap:4px;align-items:stretch}
|
||
.speedrow .speedbtn{min-height:48px;padding:0 9px;font-size:13px}
|
||
.speedrow .speedbtn.active{border-color:#ffd75e;background:var(--hl-bg);color:var(--hl-fg);box-shadow:0 0 0 2px rgba(255,215,94,.45)}
|
||
|
||
/* Акцент текущего игрока: класс на body включается только в фазах рисования
|
||
(syncDom). Вне этих фаз --accent не задан — рамка/акценты не проявляются.
|
||
box-shadow (inset) и border-color не меняют габариты — раскладка не плывёт. */
|
||
body.turn-p1{--accent:var(--p1)}
|
||
body.turn-p2{--accent:var(--p2)}
|
||
/* Рамка вокруг боковой панели (только ландшафтная раскладка body.side). */
|
||
body.side.turn-p1 #ui,body.side.turn-p2 #ui{box-shadow:inset 0 0 0 3px var(--accent)}
|
||
/* Метка фазы — акцентная рамка цветом текущего игрока. */
|
||
body.turn-p1 #turnLabel,body.turn-p2 #turnLabel{border-color:var(--accent)}
|
||
|
||
/* ===================== БОКОВАЯ ПАНЕЛЬ УПРАВЛЕНИЯ (ландшафт) =====================
|
||
В ландшафте доска занимает всю высоту и прижата влево, а управление собирается
|
||
в свободной зоне справа. Ширину зоны задаёт computeBoard (--side-w), класс
|
||
body.side включает раскладку. В портрете/узком экране всё как раньше:
|
||
#ui — display:contents, панели на своих фиксированных местах. */
|
||
#ui{display:contents}
|
||
#uiInner{display:contents}
|
||
body.side #ui{
|
||
display:flex;flex-direction:column;align-items:center;
|
||
position:fixed;top:0;bottom:0;right:0;width:var(--side-w,320px);z-index:6;
|
||
overflow-y:auto;overscroll-behavior:contain;
|
||
padding:calc(12px + var(--safe-t)) calc(14px + var(--safe-r)) calc(12px + var(--safe-b)) 14px;
|
||
}
|
||
/* margin:auto 0 центрирует стопку контролов по вертикали и остаётся корректным
|
||
при переполнении (в отличие от justify-content:center). */
|
||
body.side #uiInner{
|
||
display:flex;flex-direction:column;gap:14px;margin:auto 0;width:100%;max-width:420px;
|
||
}
|
||
body.side #hud{
|
||
position:static;left:auto;right:auto;padding:0;
|
||
flex-direction:column;align-items:stretch;justify-content:flex-start;gap:8px;
|
||
}
|
||
body.side #turnLabel{text-align:center}
|
||
body.side #hudright{justify-content:center;flex-wrap:wrap}
|
||
body.side #toolbar{
|
||
position:static;left:auto;right:auto;bottom:auto;padding:0;
|
||
display:grid;grid-template-columns:repeat(2,1fr);gap:8px;
|
||
}
|
||
body.side .toolbtn{width:100%;height:64px}
|
||
body.side #optPattern{grid-column:1 / -1;width:100%;max-width:none}
|
||
body.side #actions{
|
||
position:static;left:auto;right:auto;bottom:auto;padding:0;
|
||
flex-direction:column;justify-content:flex-start;gap:8px;
|
||
}
|
||
body.side .act{width:100%}
|
||
/* В боковой колонке ряд скорости растягиваем на всю ширину, кнопки делят её поровну. */
|
||
body.side .speedrow{width:100%}
|
||
body.side .speedrow .speedbtn{width:auto;flex:1 1 0;padding:0 4px}
|
||
|
||
/* ===================== ОВЕРЛЕИ ===================== */
|
||
.overlay{
|
||
position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;
|
||
padding:calc(var(--safe-t) + 14px) calc(var(--safe-r) + 14px) calc(var(--safe-b) + 14px) calc(var(--safe-l) + 14px);
|
||
background:var(--overlay-bg);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
|
||
}
|
||
.panel{
|
||
width:min(92vw,380px);max-height:88vh;overflow-y:auto;text-align:center;color:var(--panel-fg);
|
||
padding:22px;border-radius:22px;border:1px solid var(--line);
|
||
background:var(--panel-bg);
|
||
box-shadow:0 22px 60px rgba(0,0,0,.6),inset 0 1px 0 var(--panel-inset);
|
||
}
|
||
.title{font-size:36px;font-weight:800;letter-spacing:1px;margin-bottom:8px;
|
||
background:linear-gradient(90deg,var(--p1),#ffd75e,var(--p2));-webkit-background-clip:text;background-clip:text;color:transparent}
|
||
.panel p{font-size:14px;line-height:1.5;color:var(--muted);margin:6px 0}
|
||
.panelhead{font-size:23px;font-weight:800;margin-bottom:8px}
|
||
.big{
|
||
width:100%;margin-top:16px;padding:15px;border:none;border-radius:14px;font-size:19px;font-weight:800;
|
||
color:#241a00;background:linear-gradient(#ffd76e,#f5a623);
|
||
box-shadow:0 4px 0 #b97708,0 8px 18px rgba(245,166,35,.3);transition:transform .12s,filter .12s;
|
||
}
|
||
.big:active{transform:translateY(2px);box-shadow:0 2px 0 #b97708}
|
||
.row{display:grid;grid-template-columns:repeat(2,1fr);gap:8px;margin-top:16px}
|
||
/* Опции меню: размер поля (дропдаун), чекбокс «Замкнутая карта», ввод бюджета.
|
||
Компактная вертикаль — меню должно влезать на телефон без прокрутки. */
|
||
.opt{
|
||
display:flex;align-items:center;justify-content:space-between;gap:10px;
|
||
min-height:46px;margin-top:8px;padding:0 14px;border-radius:12px;
|
||
border:1px solid var(--line);background:var(--opt-bg);
|
||
font-size:15px;font-weight:600;text-align:left;
|
||
}
|
||
.opt input[type=checkbox]{width:22px;height:22px;accent-color:#f5a623;flex:none}
|
||
.opt input[type=number],.opt select{
|
||
min-height:36px;border-radius:8px;border:1px solid var(--line);
|
||
background:var(--input-bg);color:var(--fg);font:inherit;font-weight:700;
|
||
text-align:center;padding:0 8px;
|
||
}
|
||
.opt input[type=number]{width:118px}
|
||
.opt select{width:118px;text-align:left}
|
||
/* Опции нативного popup селектов: явные цвета темы (страховка для браузеров,
|
||
где color-scheme не покрывает список — иначе в светлой теме чёрный текст
|
||
на системном тёмном фоне). */
|
||
option{background:var(--bg);color:var(--fg)}
|
||
/* Ввод текста описания — чуть плотнее, чтобы меню влезало целиком. */
|
||
#menu .panel p{margin:4px 0;font-size:13px;line-height:1.45}
|
||
/* Раскрываемое описание (сворачиваемое по кнопке «Как играть»); используется
|
||
на сплэше-экранах: у партии на двоих (#aboutTwo) и у кампании (#aboutCampaign). */
|
||
.help{margin-top:4px;text-align:left}
|
||
.help summary{
|
||
list-style:none;cursor:pointer;display:block;padding:6px 4px;border-radius:10px;
|
||
font-size:13px;font-weight:700;color:var(--muted);transition:color .15s,background .15s;
|
||
}
|
||
.help summary::-webkit-details-marker{display:none}
|
||
.help summary::before{content:'▸ ';color:#ffd75e}
|
||
.help[open] summary::before{content:'▾ '}
|
||
.help summary:hover{color:var(--panel-fg);background:var(--opt-bg)}
|
||
.help p:first-of-type{margin-top:2px}
|
||
#gameoverText{font-size:22px;font-weight:800;margin:10px 0 4px}
|
||
#gameoverSub{font-size:14px;color:var(--muted)}
|
||
/* Оверлей передачи: панель подсвечена цветом игрока, которому передают устройство. */
|
||
#handoff.to-p2 .panel{border-color:var(--p2);box-shadow:0 22px 60px rgba(0,0,0,.6),0 0 0 2px rgba(46,204,113,.35)}
|
||
/* Панель финала — рамка цветом победителя (ничья — без изменений). */
|
||
#gameover.win-p1 .panel{border-color:var(--p1);box-shadow:0 22px 60px rgba(0,0,0,.6),0 0 0 2px rgba(231,76,60,.4)}
|
||
#gameover.win-p2 .panel{border-color:var(--p2);box-shadow:0 22px 60px rgba(0,0,0,.6),0 0 0 2px rgba(46,204,113,.4)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Доска -->
|
||
<canvas id="game"></canvas>
|
||
|
||
<!-- Управление. В ландшафте (body.side) собирается в боковую панель справа;
|
||
в портрете/узком экране — как раньше (HUD сверху, тулбар и действия снизу). -->
|
||
<div id="ui">
|
||
<div id="uiInner">
|
||
<!-- HUD: состояние/ход + счёт + настройки -->
|
||
<div id="hud">
|
||
<div id="turnLabel">Меню</div>
|
||
<div id="hudright">
|
||
<div id="budgetLabel">0 / 0</div>
|
||
<div id="scores"><span id="scoreP1">0</span><span class="scsep">:</span><span id="scoreP2">0</span></div>
|
||
<button id="btnSettings" title="Настройки">⚙️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Тулбар инструментов (активен в фазах рисования) -->
|
||
<div id="toolbar">
|
||
<button class="toolbtn active" id="toolPencil" data-tool="pencil">✏️<span>1×1</span></button>
|
||
<button class="toolbtn" id="toolBrush" data-tool="brush">🖌️<span>3×3</span></button>
|
||
<button class="toolbtn" id="toolErase1" data-tool="erase1" title="Ластик (или удерживайте правую кнопку мыши)">🧽<span>1×1</span></button>
|
||
<button class="toolbtn" id="toolErase4" data-tool="erase4" title="Ластик 4×4 (или удерживайте правую кнопку мыши)">🧹<span>4×4</span></button>
|
||
<!-- Готовый паттерн (штамп): опции строятся из PATTERNS в JS. -->
|
||
<select id="optPattern" title="Готовый паттерн (штамп). Колесо мыши над доской — поворот на 90°"><!-- опции строятся в JS --></select>
|
||
<!-- Поворот активного паттерна на 90° (для сенсорных экранов — без колеса мыши). -->
|
||
<button class="toolbtn" id="btnRotate" title="Повернуть паттерн на 90°">↻<span id="rotDeg">90°</span></button>
|
||
<!-- Режим «постановка по отпусканию» (тап): нажатие — превью, отпускание — постановка. -->
|
||
<button class="toolbtn" id="btnTapPlace" title="Постановка по отпусканию (тап)">👆<span>тап</span></button>
|
||
</div>
|
||
|
||
<!-- Контекстные действия (состав зависит от фазы; пауза — инлайн, без диалога поверх) -->
|
||
<div id="actions">
|
||
<button class="act primary" id="btnReady">Готов</button>
|
||
<button class="act" id="btnReset">Сброс</button>
|
||
<!-- Кампания: перезапуск уровня с начала (враг/бюджет/зона заново). -->
|
||
<button class="act" id="btnRestart" title="Перезапустить уровень">Заново</button>
|
||
<button class="act" id="btnPause">Пауза</button>
|
||
<div class="speedrow" id="speedRow" title="Скорость симуляции">
|
||
<button class="act speedbtn" id="speed1" data-speed="1">1×</button>
|
||
<button class="act speedbtn" id="speed2" data-speed="2">2×</button>
|
||
<button class="act speedbtn" id="speed4" data-speed="4">4×</button>
|
||
<button class="act speedbtn" id="speed8" data-speed="8">8×</button>
|
||
</div>
|
||
<button class="act primary" id="btnResume">Старт симуляции</button>
|
||
<button class="act editp1" id="btnEditP1">Дорисовать красным</button>
|
||
<button class="act editp2" id="btnEditP2">Дорисовать зелёным</button>
|
||
<button class="act danger" id="btnEnd">Завершить партию</button>
|
||
<button class="act danger" id="btnEndPause">Завершить партию</button>
|
||
<button class="act menu" id="btnPauseMenu">В меню</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Свой список паттернов вместо нативного popup <select> (см. buildPatternMenu):
|
||
на длинном списке (22 фигуры) Chromium позиционирует popup поверх курсора, и
|
||
обычный клик выбирает случайную фигуру — дропдаун «не раскрывается». -->
|
||
<div id="patternMenu" class="hidden"></div>
|
||
|
||
<!-- Стартовое меню (сплэш): краткое описание игры, тема и выбор режима.
|
||
Настройки поля (размер/torus/бюджет) живут на экране «Игра на двоих» (#setup):
|
||
они относятся только к обычной hot-seat партии. -->
|
||
<div id="menu" class="overlay">
|
||
<div class="panel">
|
||
<div class="title">Life</div>
|
||
<p>Conway's Game of Life с территориальной борьбой.</p>
|
||
<!-- Тема оформления: одна на всё приложение, поэтому остаётся на стартовом экране. -->
|
||
<label class="opt"><span>Тема</span>
|
||
<select id="optTheme">
|
||
<option value="dark">Тёмная</option>
|
||
<option value="light">Светлая</option>
|
||
</select>
|
||
</label>
|
||
<!-- Возврат к прерванной партии: виден только если есть отложенная игра.
|
||
Выделен (как основное действие вернувшегося игрока) — в отличие от равных кнопок режимов. -->
|
||
<button class="big" id="btnContinue">Продолжить партию</button>
|
||
<!-- Два равноправных режима — в одинаковом (нейтральном) стиле .act wide. -->
|
||
<button class="act wide" id="btnCampaign">Кампания</button>
|
||
<button class="act wide" id="btnPlay">Игра на двоих</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Экран «Игра на двоих» (состояние TWO_SETUP): своя справка и настройки партии. -->
|
||
<div id="setup" class="overlay hidden">
|
||
<div class="panel">
|
||
<div class="panelhead">Игра на двоих</div>
|
||
<details class="help" id="aboutTwo">
|
||
<summary>Как играть</summary>
|
||
<p>Расставьте свои клетки, передайте устройство сопернику — он расставит вслепую. Дальше поле живёт по правилам Конвея: у кого осталось больше живых клеток, тот и победил.</p>
|
||
<p>Рисуйте левой кнопкой мыши, а правой — стирайте (ластик действует, пока кнопка удержана).</p>
|
||
<p>В тулбаре есть готовые паттерны (глайдер, улей, ружьё Госпера и др.), сгруппированные по категориям: выберите фигуру в списке и кликните — она встанет целиком, левый верхний угол в точке клика. Часть, выходящая за вашу половину, не ставится (видно по превью). Кнопка «↻» в тулбаре (или колесо мыши над доской) поворачивает выбранную фигуру на 90°.</p>
|
||
<p>На сенсорном экране включите кнопку «👆 тап»: тогда нажатие только подсвечивает будущее место, а клетка или фигура ставится в момент отпускания — палец не закрывает точку постановки.</p>
|
||
</details>
|
||
<label class="opt"><span>Размер поля</span>
|
||
<select id="optSize"><!-- опции строятся из FIELD_DIMS в JS --></select>
|
||
</label>
|
||
<label class="opt"><span>Замкнутая карта (torus)</span><input type="checkbox" id="optWrap"></label>
|
||
<label class="opt"><span>Бюджет клеток</span>
|
||
<input type="number" id="optBudget" list="budgetOptions" min="1" step="1" inputmode="numeric" value="100">
|
||
</label>
|
||
<!-- Комбобокс бюджета: подсказки-пресеты в выпадающем списке, но можно ввести любое число. -->
|
||
<datalist id="budgetOptions">
|
||
<option value="100"></option>
|
||
<option value="250"></option>
|
||
<option value="500"></option>
|
||
</datalist>
|
||
<button class="big" id="btnStartTwo">Начать</button>
|
||
<button class="act wide menu" id="btnTwoBack">В меню</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Экран выбора уровня кампании (список строится из CAMPAIGN_LEVELS в JS). -->
|
||
<div id="levels" class="overlay hidden">
|
||
<div class="panel">
|
||
<div class="panelhead">Кампания</div>
|
||
<details class="help" id="aboutCampaign">
|
||
<summary>Как играть</summary>
|
||
<p>Уничтожьте все клетки врага (зелёные), расставляя свои (красные). Враг неподвижен, поле живёт по правилам Конвея; трогать врага напрямую нельзя.</p>
|
||
<p>Вы играете один, передача устройства не нужна. Бюджет клеток ограничен, победа — ноль живых клеток врага; поражение — бюджет исчерпан, а враг ещё жив.</p>
|
||
</details>
|
||
<div id="levelList"></div>
|
||
<button class="act wide menu" id="btnLevelsBack">В меню</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Передача устройства / подтверждение старта после расстановки -->
|
||
<div id="handoff" class="overlay hidden">
|
||
<div class="panel">
|
||
<div class="panelhead" id="handoffText">Передайте устройство игроку 2</div>
|
||
<p id="handoffNote">Соперник не должен видеть экран.</p>
|
||
<button class="big" id="btnHandoff">Передать</button>
|
||
<button class="act wide" id="btnHandoffBack">Вернуться к игроку 1</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Итог партии -->
|
||
<div id="gameover" class="overlay hidden">
|
||
<div class="panel">
|
||
<div class="panelhead">Итог партии</div>
|
||
<div id="gameoverText">Ничья</div>
|
||
<div id="gameoverSub"></div>
|
||
<button class="big" id="btnRevenge">Реванш</button>
|
||
<!-- Кампания: следующий уровень (только при победе и наличии следующего). -->
|
||
<button class="big" id="btnNextLevel">Следующий уровень</button>
|
||
<button class="act wide menu" id="btnGameoverMenu">В меню</button>
|
||
<button class="act wide menu" id="btnGameoverLevels">К уровням</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Подтверждение необратимого/рискованного действия (новая партия, завершение) -->
|
||
<div id="confirm" class="overlay hidden">
|
||
<div class="panel">
|
||
<div class="panelhead" id="confirmHead">Начать новую партию?</div>
|
||
<p id="confirmText">Незавершённая партия будет потеряна.</p>
|
||
<button class="big" id="btnConfirmOk">Начать новую</button>
|
||
<button class="act wide" id="btnConfirmCancel">Отмена</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
'use strict';
|
||
/* ============================================================================
|
||
ЯДРО ИГРЫ. Модель поля, симуляция, FSM hot-seat, ввод Pointer Events,
|
||
отрисовка Canvas 2D, HUD/оверлеи/тулбар, персистентность (localStorage).
|
||
============================================================================ */
|
||
|
||
// ==== КОНСТАНТЫ ====
|
||
const SAVE_KEY = 'life-save-v1'; // ключ localStorage (настройки + резюм партии; имя историческое)
|
||
const FIELD_SIZES = [40, 60, 80, 100, 120, 150, 200, 250, 300]; // доступные квадратные размеры поля
|
||
const FIELD_RECTS = [[60, 100], [100, 60]]; // прямоугольные варианты (W×H): 60×100 и 100×60
|
||
// Полный список размеров для дропдауна меню: квадраты + прямоугольники.
|
||
const FIELD_DIMS = FIELD_SIZES.map(function (n) { return [n, n]; }).concat(FIELD_RECTS);
|
||
const DEFAULT_FIELD = 60; // дефолтный размер поля 60×60
|
||
var STEPS_PER_SEC = 8; // скорость симуляции (тиков в секунду)
|
||
var speedMult = 1; // множитель скорости: 1/2/4/8 (ряд кнопок «N×», транзиентно, не в сейв)
|
||
const SAVE_VERSION = 2; // текущая версия формата сейва life-save-v1
|
||
const DEFAULT_BUDGET = 100; // дефолтный лимит клеток на игрока за партию
|
||
const PERSIST_INTERVAL_MS = 3000; // троттлинг записи сейва во время RUNNING
|
||
// Режим «Кампания»: игрок всегда цвет 1, враг (стабильные фигуры) — цвет 2.
|
||
const CAMPAIGN_PLAYER = 1;
|
||
const CAMPAIGN_ENEMY = 2;
|
||
|
||
// ==== ДАННЫЕ ====
|
||
// Палитра: фон/сетка/рамка + цвета игроков.
|
||
const COLORS = {
|
||
bg: '#111418',
|
||
grid: 'rgba(255,255,255,.06)',
|
||
border: 'rgba(255,255,255,.25)',
|
||
zoneDim: 'rgba(0,0,0,.28)', // затемнение чужой половины в фазах рисования
|
||
zoneLine: 'rgba(255,255,255,.22)', // линия границы зон рисования
|
||
eraseGhost: 'rgba(255,255,255,.22)', // ghost-превью ластика
|
||
stampOut: 'rgba(120,120,130,.55)', // клетки паттерна за границей своей зоны (вылет)
|
||
p1: '#e74c3c',
|
||
p2: '#2ecc71'
|
||
};
|
||
// Палитры canvas для светлой/тёмной темы. DOM-цвета живут в CSS-переменных
|
||
// (:root / :root[data-theme="light"]); applyTheme копирует нужную палитру в COLORS
|
||
// и выставляет data-theme на <html>. Цвета игроков (p1/p2) одинаковы в обеих темах.
|
||
const CANVAS_THEME = {
|
||
dark: {
|
||
bg: '#111418',
|
||
grid: 'rgba(255,255,255,.06)',
|
||
border: 'rgba(255,255,255,.25)',
|
||
zoneDim: 'rgba(0,0,0,.28)',
|
||
zoneLine: 'rgba(255,255,255,.22)',
|
||
eraseGhost: 'rgba(255,255,255,.22)',
|
||
stampOut: 'rgba(150,150,165,.55)'
|
||
},
|
||
light: {
|
||
bg: '#eef1f5',
|
||
grid: 'rgba(0,0,0,.07)',
|
||
border: 'rgba(0,0,0,.28)',
|
||
zoneDim: 'rgba(0,0,0,.16)',
|
||
zoneLine: 'rgba(0,0,0,.22)',
|
||
eraseGhost: 'rgba(0,0,0,.22)',
|
||
stampOut: 'rgba(90,90,105,.5)'
|
||
}
|
||
};
|
||
var theme = 'dark'; // 'dark' | 'light' — активная тема (настройка, входит в сейв)
|
||
|
||
// Применить тему: data-theme на <html>, canvas-палитра (COLORS), meta theme-color.
|
||
// Только визуал — состояние партии не трогает и сейв не пишет (это делает setTheme).
|
||
function applyTheme(name) {
|
||
theme = (name === 'light') ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
const pal = CANVAS_THEME[theme];
|
||
COLORS.bg = pal.bg;
|
||
COLORS.grid = pal.grid;
|
||
COLORS.border = pal.border;
|
||
COLORS.zoneDim = pal.zoneDim;
|
||
COLORS.zoneLine = pal.zoneLine;
|
||
COLORS.eraseGhost = pal.eraseGhost;
|
||
COLORS.stampOut = pal.stampOut;
|
||
const meta = document.querySelector('meta[name="theme-color"]');
|
||
if (meta) meta.setAttribute('content', pal.bg);
|
||
}
|
||
// Пользовательское переключение темы: применить и сохранить.
|
||
function setTheme(name) {
|
||
applyTheme(name);
|
||
syncDom();
|
||
persist();
|
||
}
|
||
// Инструменты: карандаш 1×1, кисть 3×3, ластик 1×1, ластик 4×4.
|
||
const TOOLS = [
|
||
{ id: 'pencil', name: 'Карандаш', size: 1, erase: false },
|
||
{ id: 'brush', name: 'Кисть', size: 3, erase: false },
|
||
{ id: 'erase1', name: 'Ластик', size: 1, erase: true },
|
||
{ id: 'erase4', name: 'Ластик 4×4', size: 4, erase: true }
|
||
];
|
||
|
||
// Готовые паттерны (штампы). cells — ASCII-строки: '1' = живая клетка, '.' = пусто.
|
||
// Якорь штампа — ЛЕВЫЙ ВЕРХНИЙ угол bbox (клетка под указателем). Порядок клеток
|
||
// фиксирован (сверху вниз, слева направо) — детерминированный расход бюджета.
|
||
// group — категория для группировки дропдауна (см. PATTERN_GROUPS).
|
||
// Паттерны НЕ входят в TOOLS: выбираются отдельным дропдауном #optPattern, активный
|
||
// живёт в глобале stamp (транзиентно, как speedMult — в сейв не пишется).
|
||
const PATTERNS = [
|
||
// --- Стойкие (неподвижные) ---
|
||
{ id: 'block', name: 'Блок', group: 'still', cells: ['11', '11'] },
|
||
{ id: 'beehive', name: 'Улей', group: 'still', cells: ['.11.', '1..1', '.11.'] },
|
||
{ id: 'tub', name: 'Ванна', group: 'still', cells: ['.1.', '1.1', '.1.'] },
|
||
{ id: 'boat', name: 'Лодка', group: 'still', cells: ['.11', '1.1', '.1.'] },
|
||
{ id: 'ship', name: 'Кораблик', group: 'still', cells: ['11.', '1.1', '.11'] },
|
||
{ id: 'loaf', name: 'Каравай', group: 'still', cells: ['.11.', '1..1', '.1.1', '..1.'] },
|
||
{ id: 'eater', name: 'Пожиратель', group: 'still', cells: ['11..', '1.1.', '..1.', '..11'] },
|
||
{ id: 'pond', name: 'Пруд', group: 'still', cells: ['.11.', '1..1', '1..1', '.11.'] },
|
||
{ id: 'aircraftcarrier', name: 'Авианосец', group: 'still', cells: ['11..', '1..1', '..11'] },
|
||
{ id: 'longboat', name: 'Длинная лодка', group: 'still', cells: ['.1..', '1.1.', '.1.1', '..11'] },
|
||
{ id: 'canoe', name: 'Каноэ', group: 'still', cells: ['...11', '....1', '...1.', '1.1..', '11...'] },
|
||
// --- Осцилляторы ---
|
||
{ id: 'blinker', name: 'Мигалка', group: 'osc', cells: ['111'] },
|
||
{ id: 'toad', name: 'Жаба', group: 'osc', cells: ['.111', '111.'] },
|
||
{ id: 'beacon', name: 'Маяк', group: 'osc', cells: ['11..', '11..', '..11', '..11'] },
|
||
{ id: 'clock', name: 'Часы', group: 'osc', cells: ['..1.', '1.1.', '.1.1', '.1..'] },
|
||
{ id: 'pulsar', name: 'Пульсар', group: 'osc', cells: [
|
||
'..111...111..',
|
||
'.............',
|
||
'1....1.1....1',
|
||
'1....1.1....1',
|
||
'1....1.1....1',
|
||
'..111...111..',
|
||
'.............',
|
||
'..111...111..',
|
||
'1....1.1....1',
|
||
'1....1.1....1',
|
||
'1....1.1....1',
|
||
'.............',
|
||
'..111...111..'
|
||
] },
|
||
{ id: 'pentadecathlon', name: 'Пентадекатлон', group: 'osc', cells: [
|
||
'..1....1..',
|
||
'11.1111.11',
|
||
'..1....1..'
|
||
] },
|
||
// --- Корабли ---
|
||
{ id: 'glider', name: 'Глайдер', group: 'ship', cells: ['.1.', '..1', '111'] },
|
||
{ id: 'lwss', name: 'Корабль LWSS', group: 'ship', cells: ['.1111', '1...1', '....1', '1..1.'] },
|
||
{ id: 'mwss', name: 'Корабль MWSS', group: 'ship', cells: ['...1..', '.1...1', '1.....', '1....1', '11111.'] },
|
||
{ id: 'hwss', name: 'Корабль HWSS', group: 'ship', cells: ['...11..', '.1....1', '1......', '1.....1', '111111.'] },
|
||
{ id: 'loafer', name: 'Loafer', group: 'ship', cells: ['.11..1.11', '1..1..11.', '.1.1.....', '..1......', '........1', '......111', '.....1...', '......1..', '.......11'] },
|
||
// --- Долгожители (метуселахи) ---
|
||
{ id: 'rpentomino', name: 'R-пентамино', group: 'growth', cells: ['.11', '11.', '.1.'] },
|
||
{ id: 'acorn', name: 'Жёлудь', group: 'growth', cells: ['.1.', '...1', '11..111'] },
|
||
{ id: 'diehard', name: 'Diehard', group: 'growth', cells: ['......1.', '11......', '.1...111'] },
|
||
{ id: 'thunderbird', name: 'Громовая птица', group: 'growth', cells: ['111', '...', '.1.', '.1.', '.1.'] },
|
||
{ id: 'bunnies', name: 'Кролики', group: 'growth', cells: ['1.....1.', '..1...1.', '..1..1.1', '.1.1....'] },
|
||
// --- Ружья ---
|
||
{ id: 'gun', name: 'Ружьё Госпера', group: 'gun', cells: [
|
||
'........................1...........',
|
||
'......................1.1...........',
|
||
'............11......11............11',
|
||
'...........1...1....11............11',
|
||
'11........1.....1...11..............',
|
||
'11........1...1.11....1.1...........',
|
||
'..........1.....1.......1...........',
|
||
'...........1...1....................',
|
||
'............11......................'
|
||
] },
|
||
// Ружьё Симкина: 36-клеточная версия (bbox 33×21), период 120, два ствола.
|
||
// LifeWiki даёт каноническую 29-клеточную / 33×14, но её RLE в доступных
|
||
// источниках нет — вшит вариант из patterns/simkinglidergun.rle.
|
||
{ id: 'simkingun', name: 'Ружьё Симкина', group: 'gun', cells: [
|
||
'11.....11........................',
|
||
'11.....11........................',
|
||
'.................................',
|
||
'....11...........................',
|
||
'....11...........................',
|
||
'.................................',
|
||
'.................................',
|
||
'.................................',
|
||
'.................................',
|
||
'......................11.11......',
|
||
'.....................1.....1.....',
|
||
'.....................1......1..11',
|
||
'.....................111...1...11',
|
||
'..........................1......',
|
||
'.................................',
|
||
'.................................',
|
||
'.................................',
|
||
'....................11...........',
|
||
'....................1............',
|
||
'.....................111.........',
|
||
'.......................1.........'
|
||
] }
|
||
];
|
||
|
||
// Категории паттернов для группировки дропдауна #optPattern (порядок групп и
|
||
// порядок записей внутри группы = порядок в PATTERNS). Ключ группы — поле group.
|
||
const PATTERN_GROUPS = [
|
||
{ key: 'still', label: 'Стойкие' },
|
||
{ key: 'osc', label: 'Осцилляторы' },
|
||
{ key: 'ship', label: 'Корабли' },
|
||
{ key: 'growth', label: 'Долгожители' },
|
||
{ key: 'gun', label: 'Ружья' }
|
||
];
|
||
|
||
// ==== КАМПАНИЯ: ДАННЫЕ УРОВНЕЙ ====
|
||
// На поле заранее расставлены стабильные фигуры ВРАГА (цвет 2); игрок играет
|
||
// цветом 1. Враг статичен (сам не ходит), поле живёт обычной симуляцией Конвея.
|
||
// Победа — уничтожить все клетки врага (0 клеток цвета 2); поражение — бюджет
|
||
// исчерпан, а враг ещё жив (фиксируется на авто-паузе стабильного поля). Зона
|
||
// рисования игрока — прямоугольник zone (может покрывать всё поле). Бюджет — свой.
|
||
// cells — координаты клеток ВРАГА [x, y] (компактно, без ASCII-сетки).
|
||
// Набор: 103 уровня — 7 ручных + 96 сгенерированных tools/gen-campaign-levels.mjs
|
||
// (детерминированно, id 5..100). Порядок массива = порядок уровней в игре;
|
||
// id — метаданные, по позиции не обязаны совпадать. Ручной «Издалека» (id4)
|
||
// ВСТАВЛЕН на 10-е место (индекс 9), а сгенерированные id 5..id10 сдвинуты на
|
||
// позицию вверх (индексы 3..8). Сразу за ним — ручные showcase-уровни id 101..103
|
||
// (индексы 10..12, «Флотилия»/«Погоня»/«Кролики»), затем остальные сгенерированные
|
||
// id 11..100 (индексы 13..102). Из-за вставок размеры монотонны только у
|
||
// сгенерированных (id 5..100, от 60 до 300); у ручных вставок размер свой.
|
||
// Зоны разнообразны (половина / центр / углы / вертикальная и горизонтальная
|
||
// полосы / произвольный прямоугольник). Зона «всё поле» осталась только у ручных
|
||
// «Разминки» и «Роя» — у сгенерированных её нет.
|
||
// Решаемость специально НЕ проверяется: среди уровней намеренно есть заведомо
|
||
// почти непроходимые (например с ружьём Госпера) — это задумка, а не дефект.
|
||
const CAMPAIGN_LEVELS = [
|
||
// --- 3 ручных уровня (базовая триада) ---
|
||
{ id: 1, name: 'Разминка', w: 30, h: 30, wrap: false, budget: 20,
|
||
zone: { x: 0, y: 0, w: 30, h: 30 },
|
||
cells: [[10,10], [11,10], [10,11], [11,11]] },
|
||
{ id: 2, name: 'Каравай', w: 40, h: 40, wrap: false, budget: 15,
|
||
zone: { x: 5, y: 5, w: 15, h: 15 },
|
||
cells: [[11,10], [12,10], [10,11], [13,11], [11,12], [13,12], [12,13]] },
|
||
{ id: 3, name: 'Рой', w: 60, h: 60, wrap: false, budget: 100,
|
||
zone: { x: 0, y: 0, w: 60, h: 60 },
|
||
cells: [[8,6], [9,6], [10,6], [14,6], [15,6], [16,6], [6,8], [11,8], [13,8], [18,8], [6,9], [11,9], [13,9], [18,9], [6,10], [11,10], [13,10], [18,10], [8,11], [9,11], [10,11], [14,11], [15,11], [16,11], [8,13], [9,13], [10,13], [14,13], [15,13], [16,13], [6,14], [11,14], [13,14], [18,14], [6,15], [11,15], [13,15], [18,15], [6,16], [11,16], [13,16], [18,16], [8,18], [9,18], [10,18], [14,18], [15,18], [16,18], [25,6], [26,6], [24,7], [27,7], [25,8], [26,8], [32,6], [33,6], [34,6], [39,6], [40,6], [41,6], [38,7], [39,7], [40,7], [47,6], [48,6], [46,7], [49,7], [47,8], [49,8], [48,9], [7,24], [8,24], [6,25], [9,25], [7,26], [8,26], [14,24], [15,24], [16,24], [21,24], [22,24], [23,24], [20,25], [21,25], [22,25], [29,24], [30,24], [28,25], [31,25], [29,26], [31,26], [30,27], [37,24], [38,24], [36,25], [39,25], [37,26], [38,26], [44,24], [45,24], [46,24], [7,34], [8,34], [9,34], [6,35], [7,35], [8,35], [15,34], [16,34], [14,35], [17,35], [15,36], [17,36], [16,37]] },
|
||
// --- сгенерированные (96): tools/gen-campaign-levels.mjs (SEED 0x5eed1234), id 5..100 ---
|
||
// Ручной «Издалека» вставлен на 10-е место (индекс 9); id5..id10 идут в обычном
|
||
// порядке (позиции 4..9), затем ручные showcase id 101..103 (индексы 10..12).
|
||
{ id: 5, name: 'Схема №5', w: 60, h: 60, wrap: false, budget: 68,
|
||
zone: { x: 13, y: 13, w: 33, h: 33 },
|
||
cells: [[21,40], [22,40], [23,40], [27,40], [28,40], [29,40], [19,42], [24,42], [26,42], [31,42], [19,43], [24,43], [26,43], [31,43], [19,44], [24,44], [26,44], [31,44], [21,45], [22,45], [23,45], [27,45], [28,45], [29,45], [21,47], [22,47], [23,47], [27,47], [28,47], [29,47], [19,48], [24,48], [26,48], [31,48], [19,49], [24,49], [26,49], [31,49], [19,50], [24,50], [26,50], [31,50], [21,52], [22,52], [23,52], [27,52], [28,52], [29,52], [57,1], [58,1], [59,1], [5,40], [6,40], [4,41], [7,41], [5,42], [6,42], [13,24], [14,24], [15,24], [12,25], [13,25], [14,25], [21,35], [22,35], [23,35]] },
|
||
{ id: 6, name: 'Схема №6', w: 62, h: 62, wrap: false, budget: 70,
|
||
zone: { x: 0, y: 0, w: 62, h: 31 },
|
||
cells: [[15,9], [16,9], [17,9], [21,9], [22,9], [23,9], [13,11], [18,11], [20,11], [25,11], [13,12], [18,12], [20,12], [25,12], [13,13], [18,13], [20,13], [25,13], [15,14], [16,14], [17,14], [21,14], [22,14], [23,14], [15,16], [16,16], [17,16], [21,16], [22,16], [23,16], [13,17], [18,17], [20,17], [25,17], [13,18], [18,18], [20,18], [25,18], [13,19], [18,19], [20,19], [25,19], [15,21], [16,21], [17,21], [21,21], [22,21], [23,21], [31,54], [32,54], [30,55], [33,55], [31,56], [33,56], [32,57], [49,22], [50,22], [51,22], [43,54], [44,54], [45,54], [42,55], [43,55], [44,55], [58,21], [59,21], [57,22], [60,22], [58,23], [59,23], [33,44], [34,44], [32,45], [35,45], [33,46], [34,46], [8,40], [9,40], [7,41], [10,41], [8,42], [9,42]] },
|
||
{ id: 7, name: 'Схема №7', w: 62, h: 62, wrap: false, budget: 70,
|
||
zone: { x: 14, y: 14, w: 34, h: 34 },
|
||
cells: [[23,29], [24,29], [25,29], [42,46], [43,46], [44,46], [41,47], [42,47], [43,47], [30,27], [31,27], [32,27], [29,28], [30,28], [31,28], [3,54], [4,54], [5,54], [31,24], [32,24], [33,24], [30,25], [31,25], [32,25]] },
|
||
{ id: 8, name: 'Схема №8', w: 64, h: 64, wrap: false, budget: 71,
|
||
zone: { x: 0, y: 0, w: 21, h: 25 },
|
||
cells: [[49,31], [50,31], [51,31], [55,31], [56,31], [57,31], [47,33], [52,33], [54,33], [59,33], [47,34], [52,34], [54,34], [59,34], [47,35], [52,35], [54,35], [59,35], [49,36], [50,36], [51,36], [55,36], [56,36], [57,36], [49,38], [50,38], [51,38], [55,38], [56,38], [57,38], [47,39], [52,39], [54,39], [59,39], [47,40], [52,40], [54,40], [59,40], [47,41], [52,41], [54,41], [59,41], [49,43], [50,43], [51,43], [55,43], [56,43], [57,43], [53,1], [54,1], [52,2], [55,2], [53,3], [55,3], [54,4], [43,29], [44,29], [45,29], [42,30], [43,30], [44,30], [36,21], [37,21], [35,22], [38,22], [36,23], [37,23], [1,2], [2,2], [3,2], [0,3], [1,3], [2,3]] },
|
||
{ id: 9, name: 'Схема №9', w: 66, h: 66, wrap: false, budget: 73,
|
||
zone: { x: 43, y: 28, w: 23, h: 38 },
|
||
cells: [[51,33], [52,33], [53,33], [48,34], [49,34], [47,35], [50,35], [48,36], [49,36], [3,23], [4,23], [2,24], [5,24], [3,25], [4,25], [49,7], [50,7], [48,8], [51,8], [49,9], [51,9], [50,10], [27,33], [28,33], [29,33], [26,34], [27,34], [28,34]] },
|
||
{ id: 10, name: 'Схема №10', w: 66, h: 66, wrap: false, budget: 73,
|
||
zone: { x: 40, y: 0, w: 11, h: 66 },
|
||
cells: [[24,50], [22,51], [24,51], [12,52], [13,52], [20,52], [21,52], [34,52], [35,52], [11,53], [15,53], [20,53], [21,53], [34,53], [35,53], [0,54], [1,54], [10,54], [16,54], [20,54], [21,54], [0,55], [1,55], [10,55], [14,55], [16,55], [17,55], [22,55], [24,55], [10,56], [16,56], [24,56], [11,57], [15,57], [12,58], [13,58], [54,53], [55,53], [56,53], [60,53], [61,53], [62,53], [52,55], [57,55], [59,55], [64,55], [52,56], [57,56], [59,56], [64,56], [52,57], [57,57], [59,57], [64,57], [54,58], [55,58], [56,58], [60,58], [61,58], [62,58], [54,60], [55,60], [56,60], [60,60], [61,60], [62,60], [52,61], [57,61], [59,61], [64,61], [52,62], [57,62], [59,62], [64,62], [52,63], [57,63], [59,63], [64,63], [54,65], [55,65], [56,65], [60,65], [61,65], [62,65], [42,31], [43,31], [44,31], [41,32], [42,32], [43,32], [58,6], [59,6], [57,7], [60,7], [58,8], [59,8], [55,4], [56,4], [57,4], [54,5], [55,5], [56,5], [5,61], [6,61], [4,62], [7,62], [5,63], [6,63], [29,36], [30,36], [28,37], [31,37], [29,38], [31,38], [30,39]] },
|
||
// --- ручной уровень «Издалека» (10-е место) ---
|
||
{ id: 4, name: 'Издалека', w: 60, h: 60, wrap: false, budget: 60,
|
||
zone: { x: 0, y: 0, w: 14, h: 20 },
|
||
cells: [[46,8], [44,9], [46,9], [34,10], [35,10], [42,10], [43,10], [56,10], [57,10], [33,11], [37,11], [42,11], [43,11], [56,11], [57,11], [22,12], [23,12], [32,12], [38,12], [42,12], [43,12], [22,13], [23,13], [32,13], [36,13], [38,13], [39,13], [44,13], [46,13], [32,14], [38,14], [46,14], [33,15], [37,15], [34,16], [35,16], [24,24], [25,24], [26,24], [30,24], [31,24], [32,24], [22,26], [27,26], [29,26], [34,26], [22,27], [27,27], [29,27], [34,27], [22,28], [27,28], [29,28], [34,28], [24,29], [25,29], [26,29], [30,29], [31,29], [32,29], [24,31], [25,31], [26,31], [30,31], [31,31], [32,31], [22,32], [27,32], [29,32], [34,32], [22,33], [27,33], [29,33], [34,33], [22,34], [27,34], [29,34], [34,34], [24,36], [25,36], [26,36], [30,36], [31,36], [32,36], [39,24], [40,24], [38,25], [41,25], [39,26], [40,26], [45,24], [46,24], [47,24], [44,25], [45,25], [46,25], [23,40], [24,40], [22,41], [25,41], [23,42], [25,42], [24,43], [30,44], [31,44], [32,44]] },
|
||
// --- ручные showcase-уровни (11–13-е места, сразу после «Издалека») ---
|
||
// Демонстрируют новые фигуры-штампы: движущиеся корабли MWSS/HWSS/Loafer,
|
||
// метуселаху «Кролики» и стойкие авианосец/длинная лодка/каноэ.
|
||
{ id: 101, name: 'Флотилия', w: 80, h: 80, wrap: false, budget: 120,
|
||
zone: { x: 0, y: 0, w: 40, h: 80 },
|
||
cells: [[33,18], [31,19], [35,19], [30,20], [30,21], [35,21], [30,22], [31,22], [32,22], [33,22], [34,22], [33,42], [34,42], [31,43], [36,43], [30,44], [30,45], [36,45], [30,46], [31,46], [32,46], [33,46], [34,46], [35,46], [8,62], [9,62], [8,63], [11,63], [10,64], [11,64], [19,72], [18,73], [20,73], [19,74], [21,74], [20,75], [21,75]] },
|
||
{ id: 102, name: 'Погоня', w: 90, h: 90, wrap: false, budget: 140,
|
||
zone: { x: 0, y: 0, w: 45, h: 90 },
|
||
cells: [[77,10], [76,11], [78,11], [77,12], [79,12], [78,13], [79,13], [21,25], [22,25], [25,25], [27,25], [28,25], [20,26], [23,26], [26,26], [27,26], [21,27], [23,27], [22,28], [28,29], [26,30], [27,30], [28,30], [25,31], [26,32], [27,33], [28,33], [53,55], [54,55], [57,55], [59,55], [60,55], [52,56], [55,56], [58,56], [59,56], [53,57], [55,57], [54,58], [60,59], [58,60], [59,60], [60,60], [57,61], [58,62], [59,63], [60,63], [15,78], [16,78], [16,79], [15,80], [12,81], [14,81], [12,82], [13,82]] },
|
||
{ id: 103, name: 'Кролики', w: 100, h: 100, wrap: false, budget: 160,
|
||
zone: { x: 0, y: 0, w: 50, h: 100 },
|
||
cells: [[12,12], [13,12], [12,13], [15,13], [14,14], [15,14], [40,35], [46,35], [42,36], [46,36], [42,37], [45,37], [47,37], [41,38], [43,38], [85,84], [86,84], [86,85], [85,86], [82,87], [84,87], [82,88], [83,88]] },
|
||
// --- остальные сгенерированные (id 11..100) ---
|
||
{ id: 11, name: 'Схема №11', w: 68, h: 68, wrap: false, budget: 74,
|
||
zone: { x: 0, y: 49, w: 68, h: 14 },
|
||
cells: [[30,28], [31,28], [32,28], [36,28], [37,28], [38,28], [28,30], [33,30], [35,30], [40,30], [28,31], [33,31], [35,31], [40,31], [28,32], [33,32], [35,32], [40,32], [30,33], [31,33], [32,33], [36,33], [37,33], [38,33], [30,35], [31,35], [32,35], [36,35], [37,35], [38,35], [28,36], [33,36], [35,36], [40,36], [28,37], [33,37], [35,37], [40,37], [28,38], [33,38], [35,38], [40,38], [30,40], [31,40], [32,40], [36,40], [37,40], [38,40], [39,14], [40,14], [38,15], [41,15], [39,16], [40,16], [11,31], [12,31], [10,32], [13,32], [11,33], [12,33], [60,14], [61,14], [59,15], [62,15], [60,16], [61,16], [17,8], [18,8], [19,8], [16,9], [17,9], [18,9]] },
|
||
{ id: 12, name: 'Схема №12', w: 70, h: 70, wrap: false, budget: 76,
|
||
zone: { x: 39, y: 10, w: 25, h: 23 },
|
||
cells: [[4,18], [5,18], [3,19], [6,19], [4,20], [6,20], [5,21], [22,38], [23,38], [24,38], [23,61], [24,61], [22,62], [25,62], [23,63], [25,63], [24,64], [35,34], [36,34], [37,34], [53,26], [54,26], [52,27], [55,27], [53,28], [55,28], [54,29]] },
|
||
{ id: 13, name: 'Схема №13', w: 72, h: 72, wrap: false, budget: 78,
|
||
zone: { x: 16, y: 16, w: 40, h: 40 },
|
||
cells: [[14,19], [15,19], [16,19], [13,20], [14,20], [15,20], [5,49], [6,49], [4,50], [7,50], [5,51], [6,51], [48,32], [49,32], [47,33], [50,33], [48,34], [49,34], [40,14], [41,14], [39,15], [42,15], [40,16], [41,16], [47,60], [48,60], [46,61], [49,61], [47,62], [48,62]] },
|
||
{ id: 14, name: 'Схема №14', w: 74, h: 74, wrap: false, budget: 79,
|
||
zone: { x: 0, y: 37, w: 74, h: 37 },
|
||
cells: [[20,11], [21,11], [22,11], [26,11], [27,11], [28,11], [18,13], [23,13], [25,13], [30,13], [18,14], [23,14], [25,14], [30,14], [18,15], [23,15], [25,15], [30,15], [20,16], [21,16], [22,16], [26,16], [27,16], [28,16], [20,18], [21,18], [22,18], [26,18], [27,18], [28,18], [18,19], [23,19], [25,19], [30,19], [18,20], [23,20], [25,20], [30,20], [18,21], [23,21], [25,21], [30,21], [20,23], [21,23], [22,23], [26,23], [27,23], [28,23], [10,44], [11,44], [9,45], [12,45], [10,46], [11,46], [37,10], [38,10], [36,11], [39,11], [37,12], [38,12], [13,25], [14,25], [12,26], [15,26], [13,27], [14,27], [10,35], [11,35], [9,36], [12,36], [10,37], [12,37], [11,38], [59,33], [60,33], [61,33], [58,34], [59,34], [60,34], [30,34], [31,34], [32,34]] },
|
||
{ id: 15, name: 'Схема №15', w: 74, h: 74, wrap: false, budget: 79,
|
||
zone: { x: 16, y: 16, w: 41, h: 41 },
|
||
cells: [[44,37], [45,37], [46,37], [43,38], [44,38], [45,38], [49,6], [50,6], [51,6], [27,26], [28,26], [26,27], [29,27], [27,28], [28,28], [31,10], [32,10], [30,11], [33,11], [31,12], [33,12], [32,13], [42,9], [43,9], [44,9], [41,10], [42,10], [43,10]] },
|
||
{ id: 16, name: 'Схема №16', w: 76, h: 76, wrap: false, budget: 81,
|
||
zone: { x: 0, y: 0, w: 39, h: 35 },
|
||
cells: [[42,12], [43,12], [44,12], [41,13], [42,13], [43,13], [54,63], [55,63], [56,63], [12,11], [13,11], [14,11], [11,12], [12,12], [13,12], [46,33], [47,33], [48,33], [45,34], [46,34], [47,34], [51,27], [52,27], [50,28], [53,28], [51,29], [52,29]] },
|
||
{ id: 17, name: 'Схема №17', w: 78, h: 78, wrap: false, budget: 82,
|
||
zone: { x: 40, y: 34, w: 38, h: 44 },
|
||
cells: [[39,7], [40,7], [41,7], [45,7], [46,7], [47,7], [37,9], [42,9], [44,9], [49,9], [37,10], [42,10], [44,10], [49,10], [37,11], [42,11], [44,11], [49,11], [39,12], [40,12], [41,12], [45,12], [46,12], [47,12], [39,14], [40,14], [41,14], [45,14], [46,14], [47,14], [37,15], [42,15], [44,15], [49,15], [37,16], [42,16], [44,16], [49,16], [37,17], [42,17], [44,17], [49,17], [39,19], [40,19], [41,19], [45,19], [46,19], [47,19], [14,56], [15,56], [13,57], [16,57], [14,58], [15,58], [73,65], [74,65], [72,66], [75,66], [73,67], [74,67], [15,3], [16,3], [17,3], [14,4], [15,4], [16,4], [43,70], [44,70], [45,70], [42,71], [43,71], [44,71]] },
|
||
{ id: 18, name: 'Схема №18', w: 80, h: 80, wrap: false, budget: 84,
|
||
zone: { x: 55, y: 0, w: 19, h: 80 },
|
||
cells: [[32,27], [33,27], [34,27], [38,27], [39,27], [40,27], [30,29], [35,29], [37,29], [42,29], [30,30], [35,30], [37,30], [42,30], [30,31], [35,31], [37,31], [42,31], [32,32], [33,32], [34,32], [38,32], [39,32], [40,32], [32,34], [33,34], [34,34], [38,34], [39,34], [40,34], [30,35], [35,35], [37,35], [42,35], [30,36], [35,36], [37,36], [42,36], [30,37], [35,37], [37,37], [42,37], [32,39], [33,39], [34,39], [38,39], [39,39], [40,39], [46,65], [47,65], [48,65], [45,66], [46,66], [47,66], [19,28], [20,28], [18,29], [21,29], [19,30], [20,30], [22,51], [23,51], [24,51], [21,52], [22,52], [23,52], [20,18], [21,18], [19,19], [22,19], [20,20], [22,20], [21,21], [30,9], [31,9], [32,9], [29,10], [30,10], [31,10], [64,30], [65,30], [66,30], [63,31], [64,31], [65,31]] },
|
||
{ id: 19, name: 'Схема №19', w: 82, h: 82, wrap: false, budget: 86,
|
||
zone: { x: 0, y: 54, w: 82, h: 13 },
|
||
cells: [[23,25], [24,25], [22,26], [25,26], [23,27], [25,27], [24,28], [66,39], [67,39], [68,39], [33,32], [34,32], [35,32], [32,33], [33,33], [34,33], [32,76], [33,76], [34,76], [31,77], [32,77], [33,77], [21,45], [22,45], [23,45], [20,46], [21,46], [22,46]] },
|
||
{ id: 20, name: 'Схема №20', w: 84, h: 84, wrap: false, budget: 87,
|
||
zone: { x: 26, y: 52, w: 31, h: 32 },
|
||
cells: [[51,9], [52,9], [53,9], [57,9], [58,9], [59,9], [49,11], [54,11], [56,11], [61,11], [49,12], [54,12], [56,12], [61,12], [49,13], [54,13], [56,13], [61,13], [51,14], [52,14], [53,14], [57,14], [58,14], [59,14], [51,16], [52,16], [53,16], [57,16], [58,16], [59,16], [49,17], [54,17], [56,17], [61,17], [49,18], [54,18], [56,18], [61,18], [49,19], [54,19], [56,19], [61,19], [51,21], [52,21], [53,21], [57,21], [58,21], [59,21], [72,63], [73,63], [71,64], [74,64], [72,65], [73,65], [29,80], [30,80], [28,81], [31,81], [29,82], [30,82], [17,72], [18,72], [16,73], [19,73], [17,74], [19,74], [18,75], [16,77], [17,77], [15,78], [18,78], [16,79], [17,79]] },
|
||
{ id: 21, name: 'Схема №21', w: 86, h: 86, wrap: false, budget: 89,
|
||
zone: { x: 19, y: 19, w: 47, h: 47 },
|
||
cells: [[33,44], [34,44], [32,45], [35,45], [33,46], [34,46], [79,53], [80,53], [78,54], [81,54], [79,55], [80,55], [14,73], [15,73], [16,73], [12,13], [13,13], [11,14], [14,14], [12,15], [14,15], [13,16], [78,16], [79,16], [77,17], [80,17], [78,18], [80,18], [79,19]] },
|
||
{ id: 22, name: 'Схема №22', w: 88, h: 88, wrap: false, budget: 90,
|
||
zone: { x: 0, y: 0, w: 88, h: 44 },
|
||
cells: [[38,54], [36,55], [38,55], [26,56], [27,56], [34,56], [35,56], [48,56], [49,56], [25,57], [29,57], [34,57], [35,57], [48,57], [49,57], [14,58], [15,58], [24,58], [30,58], [34,58], [35,58], [14,59], [15,59], [24,59], [28,59], [30,59], [31,59], [36,59], [38,59], [24,60], [30,60], [38,60], [25,61], [29,61], [26,62], [27,62], [39,15], [40,15], [41,15], [45,15], [46,15], [47,15], [37,17], [42,17], [44,17], [49,17], [37,18], [42,18], [44,18], [49,18], [37,19], [42,19], [44,19], [49,19], [39,20], [40,20], [41,20], [45,20], [46,20], [47,20], [39,22], [40,22], [41,22], [45,22], [46,22], [47,22], [37,23], [42,23], [44,23], [49,23], [37,24], [42,24], [44,24], [49,24], [37,25], [42,25], [44,25], [49,25], [39,27], [40,27], [41,27], [45,27], [46,27], [47,27], [18,21], [19,21], [17,22], [20,22], [18,23], [20,23], [19,24], [14,3], [15,3], [16,3], [13,4], [14,4], [15,4], [83,42], [84,42], [82,43], [85,43], [83,44], [85,44], [84,45], [58,44], [59,44], [57,45], [60,45], [58,46], [60,46], [59,47], [58,2], [59,2], [57,3], [60,3], [58,4], [59,4]] },
|
||
{ id: 23, name: 'Схема №23', w: 90, h: 90, wrap: false, budget: 92,
|
||
zone: { x: 20, y: 20, w: 50, h: 50 },
|
||
cells: [[75,4], [76,4], [77,4], [81,4], [82,4], [83,4], [73,6], [78,6], [80,6], [85,6], [73,7], [78,7], [80,7], [85,7], [73,8], [78,8], [80,8], [85,8], [75,9], [76,9], [77,9], [81,9], [82,9], [83,9], [75,11], [76,11], [77,11], [81,11], [82,11], [83,11], [73,12], [78,12], [80,12], [85,12], [73,13], [78,13], [80,13], [85,13], [73,14], [78,14], [80,14], [85,14], [75,16], [76,16], [77,16], [81,16], [82,16], [83,16], [6,84], [7,84], [8,84], [5,85], [6,85], [7,85], [35,4], [36,4], [37,4], [34,5], [35,5], [36,5], [63,69], [64,69], [62,70], [65,70], [63,71], [64,71], [29,24], [30,24], [28,25], [31,25], [29,26], [30,26], [11,79], [12,79], [13,79], [10,80], [11,80], [12,80]] },
|
||
{ id: 24, name: 'Схема №24', w: 92, h: 92, wrap: false, budget: 94,
|
||
zone: { x: 0, y: 0, w: 51, h: 34 },
|
||
cells: [[19,50], [20,50], [18,51], [21,51], [19,52], [21,52], [20,53], [85,74], [86,74], [84,75], [87,75], [85,76], [86,76], [33,36], [34,36], [32,37], [35,37], [33,38], [35,38], [34,39], [14,81], [15,81], [13,82], [16,82], [14,83], [15,83], [49,80], [50,80], [51,80], [48,81], [49,81], [50,81], [55,55], [56,55], [57,55], [54,56], [55,56], [56,56]] },
|
||
{ id: 25, name: 'Схема №25', w: 94, h: 94, wrap: false, budget: 95,
|
||
zone: { x: 65, y: 47, w: 29, h: 47 },
|
||
cells: [[66,80], [67,80], [65,81], [68,81], [66,82], [67,82], [52,11], [53,11], [54,11], [51,12], [52,12], [53,12], [50,58], [51,58], [52,58], [49,59], [50,59], [51,59], [60,22], [61,22], [62,22], [59,23], [60,23], [61,23], [21,23], [22,23], [23,23], [20,24], [21,24], [22,24], [49,3], [50,3], [51,3], [48,4], [49,4], [50,4]] },
|
||
{ id: 26, name: 'Схема №26', w: 96, h: 96, wrap: false, budget: 97,
|
||
zone: { x: 60, y: 0, w: 14, h: 96 },
|
||
cells: [[42,33], [43,33], [44,33], [48,33], [49,33], [50,33], [40,35], [45,35], [47,35], [52,35], [40,36], [45,36], [47,36], [52,36], [40,37], [45,37], [47,37], [52,37], [42,38], [43,38], [44,38], [48,38], [49,38], [50,38], [42,40], [43,40], [44,40], [48,40], [49,40], [50,40], [40,41], [45,41], [47,41], [52,41], [40,42], [45,42], [47,42], [52,42], [40,43], [45,43], [47,43], [52,43], [42,45], [43,45], [44,45], [48,45], [49,45], [50,45], [11,4], [12,4], [13,4], [10,5], [11,5], [12,5], [19,24], [20,24], [18,25], [21,25], [19,26], [20,26], [64,17], [65,17], [66,17], [28,9], [29,9], [27,10], [30,10], [28,11], [29,11], [92,1], [93,1], [94,1], [74,3], [75,3], [76,3], [73,4], [74,4], [75,4], [2,30], [3,30], [4,30], [1,31], [2,31], [3,31]] },
|
||
{ id: 27, name: 'Схема №27', w: 100, h: 100, wrap: false, budget: 100,
|
||
zone: { x: 0, y: 64, w: 100, h: 15 },
|
||
cells: [[15,12], [16,12], [14,13], [17,13], [15,14], [16,14], [53,69], [54,69], [55,69], [52,70], [53,70], [54,70], [86,40], [87,40], [88,40], [85,41], [86,41], [87,41], [42,75], [43,75], [41,76], [44,76], [42,77], [43,77], [34,75], [35,75], [33,76], [36,76], [34,77], [35,77], [34,63], [35,63], [36,63], [33,64], [34,64], [35,64]] },
|
||
{ id: 28, name: 'Схема №28', w: 102, h: 102, wrap: false, budget: 102,
|
||
zone: { x: 32, y: 21, w: 59, h: 33 },
|
||
cells: [[44,40], [45,40], [43,41], [46,41], [44,42], [45,42], [16,15], [17,15], [15,16], [18,16], [16,17], [17,17], [5,33], [6,33], [4,34], [7,34], [5,35], [7,35], [6,36], [36,56], [37,56], [38,56], [35,57], [36,57], [37,57], [65,14], [66,14], [67,14], [64,15], [65,15], [66,15], [70,25], [71,25], [72,25], [69,26], [70,26], [71,26]] },
|
||
{ id: 29, name: 'Схема №29', w: 104, h: 104, wrap: false, budget: 103,
|
||
zone: { x: 23, y: 23, w: 57, h: 57 },
|
||
cells: [[74,66], [75,66], [76,66], [80,66], [81,66], [82,66], [72,68], [77,68], [79,68], [84,68], [72,69], [77,69], [79,69], [84,69], [72,70], [77,70], [79,70], [84,70], [74,71], [75,71], [76,71], [80,71], [81,71], [82,71], [74,73], [75,73], [76,73], [80,73], [81,73], [82,73], [72,74], [77,74], [79,74], [84,74], [72,75], [77,75], [79,75], [84,75], [72,76], [77,76], [79,76], [84,76], [74,78], [75,78], [76,78], [80,78], [81,78], [82,78], [13,29], [14,29], [15,29], [39,49], [40,49], [41,49], [83,86], [84,86], [82,87], [85,87], [83,88], [85,88], [84,89], [2,35], [3,35], [1,36], [4,36], [2,37], [3,37], [74,54], [75,54], [73,55], [76,55], [74,56], [75,56]] },
|
||
{ id: 30, name: 'Схема №30', w: 106, h: 106, wrap: false, budget: 105,
|
||
zone: { x: 0, y: 53, w: 106, h: 53 },
|
||
cells: [[68,43], [69,43], [70,43], [74,43], [75,43], [76,43], [66,45], [71,45], [73,45], [78,45], [66,46], [71,46], [73,46], [78,46], [66,47], [71,47], [73,47], [78,47], [68,48], [69,48], [70,48], [74,48], [75,48], [76,48], [68,50], [69,50], [70,50], [74,50], [75,50], [76,50], [66,51], [71,51], [73,51], [78,51], [66,52], [71,52], [73,52], [78,52], [66,53], [71,53], [73,53], [78,53], [68,55], [69,55], [70,55], [74,55], [75,55], [76,55], [86,100], [87,100], [85,101], [88,101], [86,102], [87,102], [50,30], [51,30], [49,31], [52,31], [50,32], [51,32], [11,33], [12,33], [10,34], [13,34], [11,35], [12,35], [88,11], [89,11], [87,12], [90,12], [88,13], [90,13], [89,14], [83,75], [84,75], [82,76], [85,76], [83,77], [84,77], [83,96], [84,96], [85,96], [82,97], [83,97], [84,97], [88,61], [89,61], [87,62], [90,62], [88,63], [89,63]] },
|
||
{ id: 31, name: 'Схема №31', w: 108, h: 108, wrap: false, budget: 106,
|
||
zone: { x: 24, y: 24, w: 59, h: 59 },
|
||
cells: [[49,3], [50,3], [48,4], [51,4], [49,5], [51,5], [50,6], [4,32], [5,32], [3,33], [6,33], [4,34], [5,34], [93,16], [94,16], [95,16], [85,73], [86,73], [84,74], [87,74], [85,75], [87,75], [86,76], [60,40], [61,40], [59,41], [62,41], [60,42], [61,42], [52,29], [53,29], [54,29], [51,30], [52,30], [53,30]] },
|
||
{ id: 32, name: 'Схема №32', w: 110, h: 110, wrap: false, budget: 108,
|
||
zone: { x: 0, y: 0, w: 58, h: 34 },
|
||
cells: [[93,10], [94,10], [95,10], [99,10], [100,10], [101,10], [91,12], [96,12], [98,12], [103,12], [91,13], [96,13], [98,13], [103,13], [91,14], [96,14], [98,14], [103,14], [93,15], [94,15], [95,15], [99,15], [100,15], [101,15], [93,17], [94,17], [95,17], [99,17], [100,17], [101,17], [91,18], [96,18], [98,18], [103,18], [91,19], [96,19], [98,19], [103,19], [91,20], [96,20], [98,20], [103,20], [93,22], [94,22], [95,22], [99,22], [100,22], [101,22], [37,77], [38,77], [39,77], [36,78], [37,78], [38,78], [7,59], [8,59], [9,59], [47,15], [48,15], [49,15], [58,18], [59,18], [57,19], [60,19], [58,20], [59,20], [50,51], [51,51], [52,51], [49,52], [50,52], [51,52]] },
|
||
{ id: 33, name: 'Схема №33', w: 112, h: 112, wrap: false, budget: 110,
|
||
zone: { x: 62, y: 76, w: 50, h: 36 },
|
||
cells: [[90,52], [91,52], [92,52], [89,53], [90,53], [91,53], [19,67], [20,67], [21,67], [87,83], [88,83], [86,84], [89,84], [87,85], [89,85], [88,86], [44,26], [45,26], [43,27], [46,27], [44,28], [45,28], [33,1], [34,1], [32,2], [35,2], [33,3], [34,3], [24,102], [25,102], [23,103], [26,103], [24,104], [26,104], [25,105]] },
|
||
{ id: 34, name: 'Схема №34', w: 114, h: 114, wrap: false, budget: 111,
|
||
zone: { x: 67, y: 0, w: 29, h: 114 },
|
||
cells: [[29,44], [27,45], [29,45], [17,46], [18,46], [25,46], [26,46], [39,46], [40,46], [16,47], [20,47], [25,47], [26,47], [39,47], [40,47], [5,48], [6,48], [15,48], [21,48], [25,48], [26,48], [5,49], [6,49], [15,49], [19,49], [21,49], [22,49], [27,49], [29,49], [15,50], [21,50], [29,50], [16,51], [20,51], [17,52], [18,52], [58,92], [59,92], [60,92], [64,92], [65,92], [66,92], [56,94], [61,94], [63,94], [68,94], [56,95], [61,95], [63,95], [68,95], [56,96], [61,96], [63,96], [68,96], [58,97], [59,97], [60,97], [64,97], [65,97], [66,97], [58,99], [59,99], [60,99], [64,99], [65,99], [66,99], [56,100], [61,100], [63,100], [68,100], [56,101], [61,101], [63,101], [68,101], [56,102], [61,102], [63,102], [68,102], [58,104], [59,104], [60,104], [64,104], [65,104], [66,104], [42,17], [43,17], [44,17], [41,18], [42,18], [43,18], [19,31], [20,31], [18,32], [21,32], [19,33], [21,33], [20,34], [84,45], [85,45], [83,46], [86,46], [84,47], [85,47], [69,60], [70,60], [71,60], [68,61], [69,61], [70,61], [46,32], [47,32], [45,33], [48,33], [46,34], [48,34], [47,35], [36,68], [37,68], [38,68], [35,69], [36,69], [37,69]] },
|
||
{ id: 35, name: 'Схема №35', w: 118, h: 118, wrap: false, budget: 114,
|
||
zone: { x: 0, y: 93, w: 118, h: 16 },
|
||
cells: [[70,42], [71,42], [72,42], [76,42], [77,42], [78,42], [68,44], [73,44], [75,44], [80,44], [68,45], [73,45], [75,45], [80,45], [68,46], [73,46], [75,46], [80,46], [70,47], [71,47], [72,47], [76,47], [77,47], [78,47], [70,49], [71,49], [72,49], [76,49], [77,49], [78,49], [68,50], [73,50], [75,50], [80,50], [68,51], [73,51], [75,51], [80,51], [68,52], [73,52], [75,52], [80,52], [70,54], [71,54], [72,54], [76,54], [77,54], [78,54], [30,18], [31,18], [32,18], [29,19], [30,19], [31,19], [10,29], [11,29], [12,29], [9,30], [10,30], [11,30], [107,52], [108,52], [109,52], [106,53], [107,53], [108,53], [39,10], [40,10], [38,11], [41,11], [39,12], [40,12], [93,47], [94,47], [92,48], [95,48], [93,49], [94,49]] },
|
||
{ id: 36, name: 'Схема №36', w: 120, h: 120, wrap: false, budget: 116,
|
||
zone: { x: 6, y: 51, w: 72, h: 45 },
|
||
cells: [[109,71], [110,71], [108,72], [111,72], [109,73], [110,73], [19,65], [20,65], [21,65], [18,66], [19,66], [20,66], [107,67], [108,67], [109,67], [106,68], [107,68], [108,68], [29,55], [30,55], [28,56], [31,56], [29,57], [31,57], [30,58], [82,69], [83,69], [81,70], [84,70], [82,71], [84,71], [83,72], [61,89], [62,89], [63,89], [94,42], [95,42], [93,43], [96,43], [94,44], [96,44], [95,45]] },
|
||
{ id: 37, name: 'Схема №37', w: 122, h: 122, wrap: false, budget: 118,
|
||
zone: { x: 27, y: 27, w: 67, h: 67 },
|
||
cells: [[56,33], [57,33], [58,33], [23,98], [24,98], [25,98], [22,99], [23,99], [24,99], [116,10], [117,10], [118,10], [20,52], [21,52], [19,53], [22,53], [20,54], [22,54], [21,55], [87,91], [88,91], [86,92], [89,92], [87,93], [89,93], [88,94], [95,88], [96,88], [94,89], [97,89], [95,90], [96,90], [1,7], [2,7], [3,7]] },
|
||
{ id: 38, name: 'Схема №38', w: 124, h: 124, wrap: false, budget: 119,
|
||
zone: { x: 0, y: 0, w: 124, h: 62 },
|
||
cells: [[110,91], [111,91], [112,91], [116,91], [117,91], [118,91], [108,93], [113,93], [115,93], [120,93], [108,94], [113,94], [115,94], [120,94], [108,95], [113,95], [115,95], [120,95], [110,96], [111,96], [112,96], [116,96], [117,96], [118,96], [110,98], [111,98], [112,98], [116,98], [117,98], [118,98], [108,99], [113,99], [115,99], [120,99], [108,100], [113,100], [115,100], [120,100], [108,101], [113,101], [115,101], [120,101], [110,103], [111,103], [112,103], [116,103], [117,103], [118,103], [67,92], [68,92], [69,92], [66,93], [67,93], [68,93], [11,102], [12,102], [13,102], [10,103], [11,103], [12,103], [16,58], [17,58], [15,59], [18,59], [16,60], [17,60], [88,84], [89,84], [90,84], [87,85], [88,85], [89,85], [112,82], [113,82], [114,82], [111,83], [112,83], [113,83], [17,14], [18,14], [19,14], [16,15], [17,15], [18,15], [19,92], [20,92], [21,92], [25,51], [26,51], [24,52], [27,52], [25,53], [26,53]] },
|
||
{ id: 39, name: 'Схема №39', w: 126, h: 126, wrap: false, budget: 121,
|
||
zone: { x: 28, y: 28, w: 69, h: 69 },
|
||
cells: [[90,84], [91,84], [89,85], [92,85], [90,86], [91,86], [107,12], [108,12], [106,13], [109,13], [107,14], [109,14], [108,15], [95,87], [96,87], [94,88], [97,88], [95,89], [96,89], [54,15], [55,15], [56,15], [121,44], [122,44], [123,44], [120,45], [121,45], [122,45], [43,23], [44,23], [45,23], [42,24], [43,24], [44,24], [56,30], [57,30], [58,30]] },
|
||
{ id: 40, name: 'Схема №40', w: 130, h: 130, wrap: false, budget: 124,
|
||
zone: { x: 0, y: 0, w: 84, h: 44 },
|
||
cells: [[86,104], [87,104], [88,104], [85,105], [86,105], [87,105], [76,65], [77,65], [78,65], [91,48], [92,48], [90,49], [93,49], [91,50], [93,50], [92,51], [5,34], [6,34], [7,34], [127,117], [128,117], [126,118], [129,118], [127,119], [128,119], [86,96], [87,96], [88,96], [85,97], [86,97], [87,97], [125,10], [126,10], [124,11], [127,11], [125,12], [127,12], [126,13]] },
|
||
{ id: 41, name: 'Схема №41', w: 132, h: 132, wrap: false, budget: 126,
|
||
zone: { x: 76, y: 85, w: 56, h: 47 },
|
||
cells: [[49,111], [50,111], [51,111], [55,111], [56,111], [57,111], [47,113], [52,113], [54,113], [59,113], [47,114], [52,114], [54,114], [59,114], [47,115], [52,115], [54,115], [59,115], [49,116], [50,116], [51,116], [55,116], [56,116], [57,116], [49,118], [50,118], [51,118], [55,118], [56,118], [57,118], [47,119], [52,119], [54,119], [59,119], [47,120], [52,120], [54,120], [59,120], [47,121], [52,121], [54,121], [59,121], [49,123], [50,123], [51,123], [55,123], [56,123], [57,123], [1,130], [2,130], [3,130], [0,131], [1,131], [2,131], [36,49], [37,49], [35,50], [38,50], [36,51], [37,51], [114,96], [115,96], [113,97], [116,97], [114,98], [116,98], [115,99], [103,97], [104,97], [102,98], [105,98], [103,99], [104,99], [115,116], [116,116], [114,117], [117,117], [115,118], [117,118], [116,119], [77,114], [78,114], [79,114], [76,115], [77,115], [78,115]] },
|
||
{ id: 42, name: 'Схема №42', w: 134, h: 134, wrap: false, budget: 127,
|
||
zone: { x: 37, y: 0, w: 32, h: 134 },
|
||
cells: [[85,30], [86,30], [87,30], [91,30], [92,30], [93,30], [83,32], [88,32], [90,32], [95,32], [83,33], [88,33], [90,33], [95,33], [83,34], [88,34], [90,34], [95,34], [85,35], [86,35], [87,35], [91,35], [92,35], [93,35], [85,37], [86,37], [87,37], [91,37], [92,37], [93,37], [83,38], [88,38], [90,38], [95,38], [83,39], [88,39], [90,39], [95,39], [83,40], [88,40], [90,40], [95,40], [85,42], [86,42], [87,42], [91,42], [92,42], [93,42], [57,17], [58,17], [59,17], [56,18], [57,18], [58,18], [28,75], [29,75], [27,76], [30,76], [28,77], [29,77], [110,107], [111,107], [109,108], [112,108], [110,109], [112,109], [111,110], [119,54], [120,54], [121,54], [118,55], [119,55], [120,55], [55,50], [56,50], [54,51], [57,51], [55,52], [56,52], [33,23], [34,23], [32,24], [35,24], [33,25], [34,25], [120,123], [121,123], [122,123], [119,124], [120,124], [121,124], [41,120], [42,120], [43,120], [40,121], [41,121], [42,121]] },
|
||
{ id: 43, name: 'Схема №43', w: 136, h: 136, wrap: false, budget: 129,
|
||
zone: { x: 0, y: 87, w: 136, h: 34 },
|
||
cells: [[50,131], [51,131], [52,131], [49,132], [50,132], [51,132], [64,87], [65,87], [66,87], [111,133], [112,133], [113,133], [110,134], [111,134], [112,134], [101,37], [102,37], [100,38], [103,38], [101,39], [102,39], [42,68], [43,68], [41,69], [44,69], [42,70], [43,70], [62,18], [63,18], [64,18], [61,19], [62,19], [63,19], [72,93], [73,93], [71,94], [74,94], [72,95], [73,95]] },
|
||
{ id: 44, name: 'Схема №44', w: 140, h: 140, wrap: false, budget: 132,
|
||
zone: { x: 22, y: 53, w: 41, h: 79 },
|
||
cells: [[123,30], [124,30], [125,30], [129,30], [130,30], [131,30], [121,32], [126,32], [128,32], [133,32], [121,33], [126,33], [128,33], [133,33], [121,34], [126,34], [128,34], [133,34], [123,35], [124,35], [125,35], [129,35], [130,35], [131,35], [123,37], [124,37], [125,37], [129,37], [130,37], [131,37], [121,38], [126,38], [128,38], [133,38], [121,39], [126,39], [128,39], [133,39], [121,40], [126,40], [128,40], [133,40], [123,42], [124,42], [125,42], [129,42], [130,42], [131,42], [4,11], [5,11], [6,11], [3,12], [4,12], [5,12], [127,101], [128,101], [129,101], [126,102], [127,102], [128,102], [8,38], [9,38], [7,39], [10,39], [8,40], [9,40], [91,3], [92,3], [90,4], [93,4], [91,5], [93,5], [92,6], [70,112], [71,112], [72,112], [69,113], [70,113], [71,113], [26,125], [27,125], [28,125], [25,126], [26,126], [27,126]] },
|
||
{ id: 45, name: 'Схема №45', w: 142, h: 142, wrap: false, budget: 134,
|
||
zone: { x: 32, y: 32, w: 78, h: 78 },
|
||
cells: [[104,129], [105,129], [103,130], [106,130], [104,131], [105,131], [45,50], [46,50], [44,51], [47,51], [45,52], [46,52], [8,93], [9,93], [7,94], [10,94], [8,95], [9,95], [40,32], [41,32], [39,33], [42,33], [40,34], [41,34], [92,96], [93,96], [94,96], [91,97], [92,97], [93,97], [128,130], [129,130], [130,130], [127,131], [128,131], [129,131], [7,53], [8,53], [6,54], [9,54], [7,55], [8,55]] },
|
||
{ id: 46, name: 'Схема №46', w: 144, h: 144, wrap: false, budget: 135,
|
||
zone: { x: 0, y: 72, w: 144, h: 72 },
|
||
cells: [[64,98], [62,99], [64,99], [52,100], [53,100], [60,100], [61,100], [74,100], [75,100], [51,101], [55,101], [60,101], [61,101], [74,101], [75,101], [40,102], [41,102], [50,102], [56,102], [60,102], [61,102], [40,103], [41,103], [50,103], [54,103], [56,103], [57,103], [62,103], [64,103], [50,104], [56,104], [64,104], [51,105], [55,105], [52,106], [53,106], [47,45], [48,45], [49,45], [53,45], [54,45], [55,45], [45,47], [50,47], [52,47], [57,47], [45,48], [50,48], [52,48], [57,48], [45,49], [50,49], [52,49], [57,49], [47,50], [48,50], [49,50], [53,50], [54,50], [55,50], [47,52], [48,52], [49,52], [53,52], [54,52], [55,52], [45,53], [50,53], [52,53], [57,53], [45,54], [50,54], [52,54], [57,54], [45,55], [50,55], [52,55], [57,55], [47,57], [48,57], [49,57], [53,57], [54,57], [55,57], [121,3], [122,3], [123,3], [120,4], [121,4], [122,4], [53,69], [54,69], [55,69], [52,70], [53,70], [54,70], [10,123], [11,123], [9,124], [12,124], [10,125], [12,125], [11,126], [108,30], [109,30], [107,31], [110,31], [108,32], [110,32], [109,33], [36,29], [37,29], [38,29], [35,30], [36,30], [37,30], [31,125], [32,125], [33,125], [30,126], [31,126], [32,126], [130,23], [131,23], [132,23], [129,24], [130,24], [131,24]] },
|
||
{ id: 47, name: 'Схема №47', w: 148, h: 148, wrap: false, budget: 138,
|
||
zone: { x: 33, y: 33, w: 81, h: 81 },
|
||
cells: [[137,78], [138,78], [139,78], [143,78], [144,78], [145,78], [135,80], [140,80], [142,80], [147,80], [135,81], [140,81], [142,81], [147,81], [135,82], [140,82], [142,82], [147,82], [137,83], [138,83], [139,83], [143,83], [144,83], [145,83], [137,85], [138,85], [139,85], [143,85], [144,85], [145,85], [135,86], [140,86], [142,86], [147,86], [135,87], [140,87], [142,87], [147,87], [135,88], [140,88], [142,88], [147,88], [137,90], [138,90], [139,90], [143,90], [144,90], [145,90], [122,83], [123,83], [124,83], [121,84], [122,84], [123,84], [20,141], [21,141], [22,141], [19,142], [20,142], [21,142], [60,25], [61,25], [59,26], [62,26], [60,27], [61,27], [69,38], [70,38], [71,38], [68,39], [69,39], [70,39], [94,47], [95,47], [93,48], [96,48], [94,49], [95,49], [120,73], [121,73], [122,73], [119,74], [120,74], [121,74]] },
|
||
{ id: 48, name: 'Схема №48', w: 150, h: 150, wrap: false, budget: 140,
|
||
zone: { x: 0, y: 0, w: 65, h: 90 },
|
||
cells: [[106,90], [107,90], [108,90], [105,91], [106,91], [107,91], [138,144], [139,144], [140,144], [86,19], [87,19], [88,19], [85,20], [86,20], [87,20], [14,138], [15,138], [16,138], [98,15], [99,15], [100,15], [97,16], [98,16], [99,16], [126,109], [127,109], [128,109], [125,110], [126,110], [127,110], [69,7], [70,7], [68,8], [71,8], [69,9], [71,9], [70,10], [19,21], [20,21], [21,21], [18,22], [19,22], [20,22]] },
|
||
{ id: 49, name: 'Схема №49', w: 152, h: 152, wrap: false, budget: 142,
|
||
zone: { x: 104, y: 93, w: 48, h: 59 },
|
||
cells: [[114,39], [115,39], [113,40], [116,40], [114,41], [115,41], [125,39], [126,39], [124,40], [127,40], [125,41], [126,41], [82,21], [83,21], [81,22], [84,22], [82,23], [84,23], [83,24], [2,135], [3,135], [1,136], [4,136], [2,137], [3,137], [67,146], [68,146], [69,146], [91,147], [92,147], [93,147], [113,91], [114,91], [112,92], [115,92], [113,93], [114,93], [30,70], [31,70], [32,70], [29,71], [30,71], [31,71]] },
|
||
{ id: 50, name: 'Схема №50', w: 154, h: 154, wrap: false, budget: 143,
|
||
zone: { x: 41, y: 0, w: 36, h: 154 },
|
||
cells: [[96,101], [97,101], [98,101], [102,101], [103,101], [104,101], [94,103], [99,103], [101,103], [106,103], [94,104], [99,104], [101,104], [106,104], [94,105], [99,105], [101,105], [106,105], [96,106], [97,106], [98,106], [102,106], [103,106], [104,106], [96,108], [97,108], [98,108], [102,108], [103,108], [104,108], [94,109], [99,109], [101,109], [106,109], [94,110], [99,110], [101,110], [106,110], [94,111], [99,111], [101,111], [106,111], [96,113], [97,113], [98,113], [102,113], [103,113], [104,113], [142,144], [143,144], [141,145], [144,145], [142,146], [144,146], [143,147], [91,23], [92,23], [93,23], [119,137], [120,137], [118,138], [121,138], [119,139], [121,139], [120,140], [124,91], [125,91], [123,92], [126,92], [124,93], [125,93], [57,92], [58,92], [59,92], [56,93], [57,93], [58,93], [120,110], [121,110], [122,110], [119,111], [120,111], [121,111], [134,78], [135,78], [133,79], [136,79], [134,80], [135,80], [38,140], [39,140], [37,141], [40,141], [38,142], [39,142], [85,73], [86,73], [87,73]] },
|
||
{ id: 51, name: 'Схема №51', w: 158, h: 158, wrap: false, budget: 146,
|
||
zone: { x: 0, y: 68, w: 158, h: 21 },
|
||
cells: [[18,81], [19,81], [17,82], [20,82], [18,83], [19,83], [139,6], [140,6], [141,6], [138,7], [139,7], [140,7], [10,146], [11,146], [9,147], [12,147], [10,148], [12,148], [11,149], [91,96], [92,96], [93,96], [90,97], [91,97], [92,97], [91,135], [92,135], [93,135], [90,136], [91,136], [92,136], [151,46], [152,46], [150,47], [153,47], [151,48], [152,48], [105,71], [106,71], [107,71], [129,110], [130,110], [128,111], [131,111], [129,112], [130,112]] },
|
||
{ id: 52, name: 'Схема №52', w: 160, h: 160, wrap: false, budget: 148,
|
||
zone: { x: 27, y: 28, w: 109, h: 56 },
|
||
cells: [[57,74], [58,74], [56,75], [59,75], [57,76], [58,76], [146,109], [147,109], [145,110], [148,110], [146,111], [148,111], [147,112], [105,40], [106,40], [107,40], [124,74], [125,74], [126,74], [123,75], [124,75], [125,75], [10,151], [11,151], [9,152], [12,152], [10,153], [12,153], [11,154], [91,80], [92,80], [93,80], [90,81], [91,81], [92,81], [52,45], [53,45], [54,45], [51,46], [52,46], [53,46], [49,109], [50,109], [51,109]] },
|
||
{ id: 53, name: 'Схема №53', w: 162, h: 162, wrap: false, budget: 150,
|
||
zone: { x: 36, y: 36, w: 89, h: 89 },
|
||
cells: [[78,134], [79,134], [80,134], [84,134], [85,134], [86,134], [76,136], [81,136], [83,136], [88,136], [76,137], [81,137], [83,137], [88,137], [76,138], [81,138], [83,138], [88,138], [78,139], [79,139], [80,139], [84,139], [85,139], [86,139], [78,141], [79,141], [80,141], [84,141], [85,141], [86,141], [76,142], [81,142], [83,142], [88,142], [76,143], [81,143], [83,143], [88,143], [76,144], [81,144], [83,144], [88,144], [78,146], [79,146], [80,146], [84,146], [85,146], [86,146], [75,93], [76,93], [74,94], [77,94], [75,95], [76,95], [111,132], [112,132], [113,132], [8,93], [9,93], [7,94], [10,94], [8,95], [9,95], [44,47], [45,47], [43,48], [46,48], [44,49], [46,49], [45,50], [26,98], [27,98], [28,98], [25,99], [26,99], [27,99], [126,10], [127,10], [125,11], [128,11], [126,12], [128,12], [127,13], [42,149], [43,149], [44,149], [41,150], [42,150], [43,150]] },
|
||
{ id: 54, name: 'Схема №54', w: 166, h: 166, wrap: false, budget: 153,
|
||
zone: { x: 0, y: 0, w: 166, h: 83 },
|
||
cells: [[114,5], [115,5], [116,5], [120,5], [121,5], [122,5], [112,7], [117,7], [119,7], [124,7], [112,8], [117,8], [119,8], [124,8], [112,9], [117,9], [119,9], [124,9], [114,10], [115,10], [116,10], [120,10], [121,10], [122,10], [114,12], [115,12], [116,12], [120,12], [121,12], [122,12], [112,13], [117,13], [119,13], [124,13], [112,14], [117,14], [119,14], [124,14], [112,15], [117,15], [119,15], [124,15], [114,17], [115,17], [116,17], [120,17], [121,17], [122,17], [94,15], [95,15], [96,15], [93,16], [94,16], [95,16], [19,11], [20,11], [18,12], [21,12], [19,13], [21,13], [20,14], [160,107], [161,107], [159,108], [162,108], [160,109], [162,109], [161,110], [132,79], [133,79], [131,80], [134,80], [132,81], [133,81], [89,158], [90,158], [91,158], [88,159], [89,159], [90,159], [44,119], [45,119], [43,120], [46,120], [44,121], [46,121], [45,122], [30,129], [31,129], [32,129], [29,130], [30,130], [31,130], [80,40], [81,40], [82,40], [32,111], [33,111], [34,111]] },
|
||
{ id: 55, name: 'Схема №55', w: 168, h: 168, wrap: false, budget: 154,
|
||
zone: { x: 38, y: 38, w: 92, h: 92 },
|
||
cells: [[54,144], [55,144], [53,145], [56,145], [54,146], [56,146], [55,147], [63,141], [64,141], [65,141], [62,142], [63,142], [64,142], [15,106], [16,106], [17,106], [14,107], [15,107], [16,107], [15,70], [16,70], [14,71], [17,71], [15,72], [16,72], [155,102], [156,102], [157,102], [154,103], [155,103], [156,103], [143,52], [144,52], [145,52], [24,124], [25,124], [23,125], [26,125], [24,126], [26,126], [25,127], [125,20], [126,20], [127,20]] },
|
||
{ id: 56, name: 'Схема №56', w: 170, h: 170, wrap: false, budget: 156,
|
||
zone: { x: 0, y: 0, w: 99, h: 97 },
|
||
cells: [[24,131], [25,131], [26,131], [30,131], [31,131], [32,131], [22,133], [27,133], [29,133], [34,133], [22,134], [27,134], [29,134], [34,134], [22,135], [27,135], [29,135], [34,135], [24,136], [25,136], [26,136], [30,136], [31,136], [32,136], [24,138], [25,138], [26,138], [30,138], [31,138], [32,138], [22,139], [27,139], [29,139], [34,139], [22,140], [27,140], [29,140], [34,140], [22,141], [27,141], [29,141], [34,141], [24,143], [25,143], [26,143], [30,143], [31,143], [32,143], [97,115], [98,115], [96,116], [99,116], [97,117], [98,117], [12,35], [13,35], [14,35], [53,138], [54,138], [55,138], [41,57], [42,57], [43,57], [69,163], [70,163], [68,164], [71,164], [69,165], [71,165], [70,166], [145,10], [146,10], [144,11], [147,11], [145,12], [146,12], [40,146], [41,146], [39,147], [42,147], [40,148], [41,148]] },
|
||
{ id: 57, name: 'Схема №57', w: 174, h: 174, wrap: false, budget: 159,
|
||
zone: { x: 68, y: 67, w: 106, h: 107 },
|
||
cells: [[37,145], [38,145], [36,146], [39,146], [37,147], [38,147], [51,72], [52,72], [53,72], [161,37], [162,37], [160,38], [163,38], [161,39], [162,39], [66,21], [67,21], [65,22], [68,22], [66,23], [67,23], [163,83], [164,83], [165,83], [162,84], [163,84], [164,84], [47,126], [48,126], [49,126], [46,127], [47,127], [48,127], [103,152], [104,152], [102,153], [105,153], [103,154], [105,154], [104,155], [142,25], [143,25], [144,25]] },
|
||
{ id: 58, name: 'Схема №58', w: 176, h: 176, wrap: false, budget: 161,
|
||
zone: { x: 64, y: 0, w: 24, h: 176 },
|
||
cells: [[97,154], [95,155], [97,155], [85,156], [86,156], [93,156], [94,156], [107,156], [108,156], [84,157], [88,157], [93,157], [94,157], [107,157], [108,157], [73,158], [74,158], [83,158], [89,158], [93,158], [94,158], [73,159], [74,159], [83,159], [87,159], [89,159], [90,159], [95,159], [97,159], [83,160], [89,160], [97,160], [84,161], [88,161], [85,162], [86,162], [17,25], [18,25], [19,25], [23,25], [24,25], [25,25], [15,27], [20,27], [22,27], [27,27], [15,28], [20,28], [22,28], [27,28], [15,29], [20,29], [22,29], [27,29], [17,30], [18,30], [19,30], [23,30], [24,30], [25,30], [17,32], [18,32], [19,32], [23,32], [24,32], [25,32], [15,33], [20,33], [22,33], [27,33], [15,34], [20,34], [22,34], [27,34], [15,35], [20,35], [22,35], [27,35], [17,37], [18,37], [19,37], [23,37], [24,37], [25,37], [67,171], [68,171], [69,171], [66,172], [67,172], [68,172], [37,11], [38,11], [39,11], [36,12], [37,12], [38,12], [139,126], [140,126], [138,127], [141,127], [139,128], [140,128], [30,36], [31,36], [32,36], [29,37], [30,37], [31,37], [4,80], [5,80], [3,81], [6,81], [4,82], [5,82], [77,103], [78,103], [76,104], [79,104], [77,105], [78,105], [106,112], [107,112], [108,112], [105,113], [106,113], [107,113], [89,55], [90,55], [91,55]] },
|
||
{ id: 59, name: 'Схема №59', w: 178, h: 178, wrap: false, budget: 162,
|
||
zone: { x: 0, y: 105, w: 178, h: 41 },
|
||
cells: [[122,156], [123,156], [124,156], [128,156], [129,156], [130,156], [120,158], [125,158], [127,158], [132,158], [120,159], [125,159], [127,159], [132,159], [120,160], [125,160], [127,160], [132,160], [122,161], [123,161], [124,161], [128,161], [129,161], [130,161], [122,163], [123,163], [124,163], [128,163], [129,163], [130,163], [120,164], [125,164], [127,164], [132,164], [120,165], [125,165], [127,165], [132,165], [120,166], [125,166], [127,166], [132,166], [122,168], [123,168], [124,168], [128,168], [129,168], [130,168], [25,49], [26,49], [27,49], [24,50], [25,50], [26,50], [76,53], [77,53], [75,54], [78,54], [76,55], [78,55], [77,56], [78,66], [79,66], [80,66], [77,67], [78,67], [79,67], [106,69], [107,69], [108,69], [105,70], [106,70], [107,70], [37,35], [38,35], [36,36], [39,36], [37,37], [39,37], [38,38], [100,162], [101,162], [102,162], [17,144], [18,144], [19,144]] },
|
||
{ id: 60, name: 'Схема №60', w: 182, h: 182, wrap: false, budget: 166,
|
||
zone: { x: 49, y: 34, w: 61, h: 106 },
|
||
cells: [[3,171], [4,171], [2,172], [5,172], [3,173], [5,173], [4,174], [41,86], [42,86], [40,87], [43,87], [41,88], [43,88], [42,89], [30,150], [31,150], [29,151], [32,151], [30,152], [32,152], [31,153], [177,104], [178,104], [179,104], [176,105], [177,105], [178,105], [62,63], [63,63], [61,64], [64,64], [62,65], [63,65], [139,138], [140,138], [141,138], [33,166], [34,166], [35,166], [32,167], [33,167], [34,167], [179,78], [180,78], [181,78], [178,79], [179,79], [180,79], [155,52], [156,52], [157,52], [154,53], [155,53], [156,53]] },
|
||
{ id: 61, name: 'Схема №61', w: 184, h: 184, wrap: false, budget: 167,
|
||
zone: { x: 41, y: 41, w: 101, h: 101 },
|
||
cells: [[159,98], [160,98], [161,98], [158,99], [159,99], [160,99], [177,20], [178,20], [176,21], [179,21], [177,22], [178,22], [133,50], [134,50], [135,50], [132,51], [133,51], [134,51], [111,117], [112,117], [113,117], [110,118], [111,118], [112,118], [42,79], [43,79], [44,79], [41,80], [42,80], [43,80], [116,131], [117,131], [115,132], [118,132], [116,133], [118,133], [117,134], [126,138], [127,138], [128,138], [39,117], [40,117], [38,118], [41,118], [39,119], [41,119], [40,120], [175,152], [176,152], [177,152], [174,153], [175,153], [176,153]] },
|
||
{ id: 62, name: 'Схема №62', w: 188, h: 188, wrap: false, budget: 170,
|
||
zone: { x: 0, y: 94, w: 188, h: 94 },
|
||
cells: [[142,20], [143,20], [144,20], [148,20], [149,20], [150,20], [140,22], [145,22], [147,22], [152,22], [140,23], [145,23], [147,23], [152,23], [140,24], [145,24], [147,24], [152,24], [142,25], [143,25], [144,25], [148,25], [149,25], [150,25], [142,27], [143,27], [144,27], [148,27], [149,27], [150,27], [140,28], [145,28], [147,28], [152,28], [140,29], [145,29], [147,29], [152,29], [140,30], [145,30], [147,30], [152,30], [142,32], [143,32], [144,32], [148,32], [149,32], [150,32], [115,68], [116,68], [117,68], [114,69], [115,69], [116,69], [32,16], [33,16], [31,17], [34,17], [32,18], [34,18], [33,19], [91,32], [92,32], [90,33], [93,33], [91,34], [92,34], [168,43], [169,43], [167,44], [170,44], [168,45], [169,45], [36,116], [37,116], [35,117], [38,117], [36,118], [38,118], [37,119], [45,19], [46,19], [47,19], [23,109], [24,109], [22,110], [25,110], [23,111], [24,111], [165,178], [166,178], [164,179], [167,179], [165,180], [166,180], [85,0], [86,0], [87,0], [84,1], [85,1], [86,1], [72,60], [73,60], [74,60]] },
|
||
{ id: 63, name: 'Схема №63', w: 190, h: 190, wrap: false, budget: 172,
|
||
zone: { x: 42, y: 42, w: 105, h: 105 },
|
||
cells: [[21,139], [22,139], [23,139], [149,3], [150,3], [148,4], [151,4], [149,5], [150,5], [58,71], [59,71], [57,72], [60,72], [58,73], [59,73], [145,63], [146,63], [144,64], [147,64], [145,65], [147,65], [146,66], [72,144], [73,144], [74,144], [71,145], [72,145], [73,145], [20,156], [21,156], [22,156], [19,157], [20,157], [21,157], [38,143], [39,143], [40,143], [37,144], [38,144], [39,144], [174,120], [175,120], [173,121], [176,121], [174,122], [176,122], [175,123], [156,81], [157,81], [155,82], [158,82], [156,83], [157,83]] },
|
||
{ id: 64, name: 'Схема №64', w: 192, h: 192, wrap: false, budget: 174,
|
||
zone: { x: 0, y: 0, w: 99, h: 63 },
|
||
cells: [[89,127], [90,127], [91,127], [88,128], [89,128], [90,128], [91,74], [92,74], [93,74], [90,75], [91,75], [92,75], [86,144], [87,144], [88,144], [85,145], [86,145], [87,145], [54,152], [55,152], [56,152], [67,63], [68,63], [66,64], [69,64], [67,65], [68,65], [156,167], [157,167], [158,167], [74,164], [75,164], [73,165], [76,165], [74,166], [76,166], [75,167], [34,26], [35,26], [36,26], [33,27], [34,27], [35,27], [8,36], [9,36], [10,36], [7,37], [8,37], [9,37]] },
|
||
{ id: 65, name: 'Схема №65', w: 196, h: 196, wrap: false, budget: 177,
|
||
zone: { x: 99, y: 77, w: 97, h: 119 },
|
||
cells: [[10,62], [11,62], [12,62], [16,62], [17,62], [18,62], [8,64], [13,64], [15,64], [20,64], [8,65], [13,65], [15,65], [20,65], [8,66], [13,66], [15,66], [20,66], [10,67], [11,67], [12,67], [16,67], [17,67], [18,67], [10,69], [11,69], [12,69], [16,69], [17,69], [18,69], [8,70], [13,70], [15,70], [20,70], [8,71], [13,71], [15,71], [20,71], [8,72], [13,72], [15,72], [20,72], [10,74], [11,74], [12,74], [16,74], [17,74], [18,74], [132,172], [133,172], [131,173], [134,173], [132,174], [133,174], [150,19], [151,19], [152,19], [149,20], [150,20], [151,20], [22,100], [23,100], [21,101], [24,101], [22,102], [23,102], [162,177], [163,177], [161,178], [164,178], [162,179], [164,179], [163,180], [107,177], [108,177], [109,177], [106,178], [107,178], [108,178], [169,164], [170,164], [168,165], [171,165], [169,166], [171,166], [170,167], [55,168], [56,168], [54,169], [57,169], [55,170], [57,170], [56,171], [158,98], [159,98], [157,99], [160,99], [158,100], [160,100], [159,101]] },
|
||
{ id: 66, name: 'Схема №66', w: 198, h: 198, wrap: false, budget: 178,
|
||
zone: { x: 85, y: 0, w: 34, h: 198 },
|
||
cells: [[100,51], [101,51], [102,51], [106,51], [107,51], [108,51], [98,53], [103,53], [105,53], [110,53], [98,54], [103,54], [105,54], [110,54], [98,55], [103,55], [105,55], [110,55], [100,56], [101,56], [102,56], [106,56], [107,56], [108,56], [100,58], [101,58], [102,58], [106,58], [107,58], [108,58], [98,59], [103,59], [105,59], [110,59], [98,60], [103,60], [105,60], [110,60], [98,61], [103,61], [105,61], [110,61], [100,63], [101,63], [102,63], [106,63], [107,63], [108,63], [63,59], [64,59], [62,60], [65,60], [63,61], [65,61], [64,62], [140,19], [141,19], [142,19], [55,116], [56,116], [57,116], [137,80], [138,80], [136,81], [139,81], [137,82], [138,82], [106,20], [107,20], [105,21], [108,21], [106,22], [107,22], [185,95], [186,95], [184,96], [187,96], [185,97], [186,97], [35,31], [36,31], [37,31], [175,69], [176,69], [174,70], [177,70], [175,71], [176,71], [163,55], [164,55], [162,56], [165,56], [163,57], [164,57], [79,147], [80,147], [78,148], [81,148], [79,149], [80,149]] },
|
||
{ id: 67, name: 'Схема №67', w: 202, h: 202, wrap: false, budget: 182,
|
||
zone: { x: 0, y: 159, w: 202, h: 41 },
|
||
cells: [[58,20], [59,20], [57,21], [60,21], [58,22], [59,22], [36,107], [37,107], [35,108], [38,108], [36,109], [37,109], [4,147], [5,147], [6,147], [47,118], [48,118], [46,119], [49,119], [47,120], [48,120], [85,17], [86,17], [84,18], [87,18], [85,19], [87,19], [86,20], [94,125], [95,125], [93,126], [96,126], [94,127], [95,127], [135,162], [136,162], [137,162], [148,32], [149,32], [147,33], [150,33], [148,34], [149,34], [26,134], [27,134], [25,135], [28,135], [26,136], [27,136]] },
|
||
{ id: 68, name: 'Схема №68', w: 204, h: 204, wrap: false, budget: 183,
|
||
zone: { x: 15, y: 53, w: 133, h: 68 },
|
||
cells: [[144,22], [145,22], [146,22], [150,22], [151,22], [152,22], [142,24], [147,24], [149,24], [154,24], [142,25], [147,25], [149,25], [154,25], [142,26], [147,26], [149,26], [154,26], [144,27], [145,27], [146,27], [150,27], [151,27], [152,27], [144,29], [145,29], [146,29], [150,29], [151,29], [152,29], [142,30], [147,30], [149,30], [154,30], [142,31], [147,31], [149,31], [154,31], [142,32], [147,32], [149,32], [154,32], [144,34], [145,34], [146,34], [150,34], [151,34], [152,34], [0,142], [1,142], [2,142], [142,117], [143,117], [141,118], [144,118], [142,119], [143,119], [116,85], [117,85], [115,86], [118,86], [116,87], [117,87], [14,67], [15,67], [16,67], [13,68], [14,68], [15,68], [75,146], [76,146], [77,146], [74,147], [75,147], [76,147], [4,49], [5,49], [6,49], [3,50], [4,50], [5,50], [145,120], [146,120], [147,120], [144,121], [145,121], [146,121], [153,82], [154,82], [155,82], [152,83], [153,83], [154,83]] },
|
||
{ id: 69, name: 'Схема №69', w: 206, h: 206, wrap: false, budget: 185,
|
||
zone: { x: 46, y: 46, w: 113, h: 113 },
|
||
cells: [[170,43], [171,43], [169,44], [172,44], [170,45], [171,45], [136,60], [137,60], [135,61], [138,61], [136,62], [137,62], [28,157], [29,157], [27,158], [30,158], [28,159], [30,159], [29,160], [144,57], [145,57], [146,57], [143,58], [144,58], [145,58], [144,169], [145,169], [146,169], [143,170], [144,170], [145,170], [129,8], [130,8], [128,9], [131,9], [129,10], [131,10], [130,11], [16,12], [17,12], [15,13], [18,13], [16,14], [18,14], [17,15], [167,119], [168,119], [169,119], [166,120], [167,120], [168,120], [119,12], [120,12], [118,13], [121,13], [119,14], [120,14]] },
|
||
{ id: 70, name: 'Схема №70', w: 210, h: 210, wrap: false, budget: 188,
|
||
zone: { x: 0, y: 0, w: 210, h: 105 },
|
||
cells: [[175,92], [173,93], [175,93], [163,94], [164,94], [171,94], [172,94], [185,94], [186,94], [162,95], [166,95], [171,95], [172,95], [185,95], [186,95], [151,96], [152,96], [161,96], [167,96], [171,96], [172,96], [151,97], [152,97], [161,97], [165,97], [167,97], [168,97], [173,97], [175,97], [161,98], [167,98], [175,98], [162,99], [166,99], [163,100], [164,100], [17,77], [18,77], [19,77], [23,77], [24,77], [25,77], [15,79], [20,79], [22,79], [27,79], [15,80], [20,80], [22,80], [27,80], [15,81], [20,81], [22,81], [27,81], [17,82], [18,82], [19,82], [23,82], [24,82], [25,82], [17,84], [18,84], [19,84], [23,84], [24,84], [25,84], [15,85], [20,85], [22,85], [27,85], [15,86], [20,86], [22,86], [27,86], [15,87], [20,87], [22,87], [27,87], [17,89], [18,89], [19,89], [23,89], [24,89], [25,89], [119,35], [120,35], [121,35], [65,194], [66,194], [67,194], [73,190], [74,190], [75,190], [72,191], [73,191], [74,191], [80,53], [81,53], [79,54], [82,54], [80,55], [81,55], [36,57], [37,57], [38,57], [5,21], [6,21], [4,22], [7,22], [5,23], [6,23], [134,19], [135,19], [133,20], [136,20], [134,21], [136,21], [135,22], [59,143], [60,143], [58,144], [61,144], [59,145], [60,145], [192,39], [193,39], [194,39], [191,40], [192,40], [193,40], [140,79], [141,79], [139,80], [142,80], [140,81], [141,81]] },
|
||
{ id: 71, name: 'Схема №71', w: 212, h: 212, wrap: false, budget: 190,
|
||
zone: { x: 47, y: 47, w: 117, h: 117 },
|
||
cells: [[166,117], [167,117], [168,117], [172,117], [173,117], [174,117], [164,119], [169,119], [171,119], [176,119], [164,120], [169,120], [171,120], [176,120], [164,121], [169,121], [171,121], [176,121], [166,122], [167,122], [168,122], [172,122], [173,122], [174,122], [166,124], [167,124], [168,124], [172,124], [173,124], [174,124], [164,125], [169,125], [171,125], [176,125], [164,126], [169,126], [171,126], [176,126], [164,127], [169,127], [171,127], [176,127], [166,129], [167,129], [168,129], [172,129], [173,129], [174,129], [27,107], [28,107], [26,108], [29,108], [27,109], [28,109], [179,24], [180,24], [178,25], [181,25], [179,26], [180,26], [57,50], [58,50], [56,51], [59,51], [57,52], [58,52], [41,112], [42,112], [40,113], [43,113], [41,114], [42,114], [60,55], [61,55], [62,55], [59,56], [60,56], [61,56], [85,161], [86,161], [87,161], [84,162], [85,162], [86,162], [26,17], [27,17], [28,17], [25,18], [26,18], [27,18], [116,53], [117,53], [118,53], [83,126], [84,126], [82,127], [85,127], [83,128], [84,128]] },
|
||
{ id: 72, name: 'Схема №72', w: 216, h: 216, wrap: false, budget: 193,
|
||
zone: { x: 0, y: 0, w: 136, h: 96 },
|
||
cells: [[136,50], [137,50], [135,51], [138,51], [136,52], [137,52], [15,115], [16,115], [17,115], [14,116], [15,116], [16,116], [165,116], [166,116], [167,116], [164,117], [165,117], [166,117], [56,179], [57,179], [55,180], [58,180], [56,181], [57,181], [99,202], [100,202], [101,202], [98,203], [99,203], [100,203], [91,74], [92,74], [90,75], [93,75], [91,76], [92,76], [121,184], [122,184], [120,185], [123,185], [121,186], [122,186], [170,157], [171,157], [172,157], [169,158], [170,158], [171,158], [37,147], [38,147], [39,147], [52,204], [53,204], [54,204], [51,205], [52,205], [53,205]] },
|
||
{ id: 73, name: 'Схема №73', w: 218, h: 218, wrap: false, budget: 194,
|
||
zone: { x: 137, y: 124, w: 81, h: 94 },
|
||
cells: [[14,15], [15,15], [16,15], [150,132], [151,132], [149,133], [152,133], [150,134], [151,134], [54,210], [55,210], [53,211], [56,211], [54,212], [55,212], [177,72], [178,72], [176,73], [179,73], [177,74], [179,74], [178,75], [165,100], [166,100], [164,101], [167,101], [165,102], [167,102], [166,103], [137,18], [138,18], [139,18], [136,19], [137,19], [138,19], [186,213], [187,213], [188,213], [170,2], [171,2], [169,3], [172,3], [170,4], [171,4], [212,67], [213,67], [214,67], [211,68], [212,68], [213,68], [28,34], [29,34], [27,35], [30,35], [28,36], [29,36]] },
|
||
{ id: 74, name: 'Схема №74', w: 222, h: 222, wrap: false, budget: 198,
|
||
zone: { x: 134, y: 0, w: 39, h: 222 },
|
||
cells: [[211,135], [212,135], [213,135], [217,135], [218,135], [219,135], [209,137], [214,137], [216,137], [221,137], [209,138], [214,138], [216,138], [221,138], [209,139], [214,139], [216,139], [221,139], [211,140], [212,140], [213,140], [217,140], [218,140], [219,140], [211,142], [212,142], [213,142], [217,142], [218,142], [219,142], [209,143], [214,143], [216,143], [221,143], [209,144], [214,144], [216,144], [221,144], [209,145], [214,145], [216,145], [221,145], [211,147], [212,147], [213,147], [217,147], [218,147], [219,147], [106,44], [107,44], [108,44], [105,45], [106,45], [107,45], [165,218], [166,218], [164,219], [167,219], [165,220], [167,220], [166,221], [75,183], [76,183], [77,183], [74,184], [75,184], [76,184], [111,56], [112,56], [113,56], [110,57], [111,57], [112,57], [161,184], [162,184], [160,185], [163,185], [161,186], [162,186], [181,98], [182,98], [183,98], [209,93], [210,93], [208,94], [211,94], [209,95], [211,95], [210,96], [200,184], [201,184], [202,184], [199,185], [200,185], [201,185], [26,129], [27,129], [28,129], [25,130], [26,130], [27,130], [48,180], [49,180], [50,180], [47,181], [48,181], [49,181], [142,21], [143,21], [141,22], [144,22], [142,23], [143,23]] },
|
||
{ id: 75, name: 'Схема №75', w: 224, h: 224, wrap: false, budget: 199,
|
||
zone: { x: 0, y: 141, w: 224, h: 31 },
|
||
cells: [[96,149], [97,149], [95,150], [98,150], [96,151], [97,151], [207,200], [208,200], [209,200], [206,201], [207,201], [208,201], [169,43], [170,43], [171,43], [168,44], [169,44], [170,44], [64,91], [65,91], [66,91], [113,134], [114,134], [112,135], [115,135], [113,136], [114,136], [77,115], [78,115], [79,115], [5,75], [6,75], [4,76], [7,76], [5,77], [6,77], [217,22], [218,22], [216,23], [219,23], [217,24], [218,24], [22,148], [23,148], [24,148], [199,16], [200,16], [198,17], [201,17], [199,18], [200,18]] },
|
||
{ id: 76, name: 'Схема №76', w: 228, h: 228, wrap: false, budget: 202,
|
||
zone: { x: 106, y: 15, w: 60, h: 135 },
|
||
cells: [[187,136], [188,136], [189,136], [186,137], [187,137], [188,137], [14,98], [15,98], [16,98], [116,3], [117,3], [118,3], [115,4], [116,4], [117,4], [46,105], [47,105], [48,105], [45,106], [46,106], [47,106], [48,71], [49,71], [47,72], [50,72], [48,73], [50,73], [49,74], [133,217], [134,217], [135,217], [132,218], [133,218], [134,218], [212,48], [213,48], [211,49], [214,49], [212,50], [213,50], [88,31], [89,31], [87,32], [90,32], [88,33], [89,33], [203,221], [204,221], [202,222], [205,222], [203,223], [204,223], [18,130], [19,130], [20,130], [17,131], [18,131], [19,131]] },
|
||
{ id: 77, name: 'Схема №77', w: 230, h: 230, wrap: false, budget: 204,
|
||
zone: { x: 51, y: 51, w: 127, h: 127 },
|
||
cells: [[136,139], [137,139], [138,139], [142,139], [143,139], [144,139], [134,141], [139,141], [141,141], [146,141], [134,142], [139,142], [141,142], [146,142], [134,143], [139,143], [141,143], [146,143], [136,144], [137,144], [138,144], [142,144], [143,144], [144,144], [136,146], [137,146], [138,146], [142,146], [143,146], [144,146], [134,147], [139,147], [141,147], [146,147], [134,148], [139,148], [141,148], [146,148], [134,149], [139,149], [141,149], [146,149], [136,151], [137,151], [138,151], [142,151], [143,151], [144,151], [68,67], [69,67], [70,67], [67,68], [68,68], [69,68], [159,61], [160,61], [158,62], [161,62], [159,63], [161,63], [160,64], [211,112], [212,112], [213,112], [165,80], [166,80], [167,80], [164,81], [165,81], [166,81], [66,91], [67,91], [68,91], [65,92], [66,92], [67,92], [122,183], [123,183], [121,184], [124,184], [122,185], [124,185], [123,186], [176,107], [177,107], [175,108], [178,108], [176,109], [177,109], [76,182], [77,182], [78,182], [83,197], [84,197], [85,197], [82,198], [83,198], [84,198]] },
|
||
{ id: 78, name: 'Схема №78', w: 234, h: 234, wrap: false, budget: 207,
|
||
zone: { x: 0, y: 117, w: 234, h: 117 },
|
||
cells: [[147,149], [148,149], [149,149], [153,149], [154,149], [155,149], [145,151], [150,151], [152,151], [157,151], [145,152], [150,152], [152,152], [157,152], [145,153], [150,153], [152,153], [157,153], [147,154], [148,154], [149,154], [153,154], [154,154], [155,154], [147,156], [148,156], [149,156], [153,156], [154,156], [155,156], [145,157], [150,157], [152,157], [157,157], [145,158], [150,158], [152,158], [157,158], [145,159], [150,159], [152,159], [157,159], [147,161], [148,161], [149,161], [153,161], [154,161], [155,161], [183,52], [184,52], [185,52], [182,53], [183,53], [184,53], [15,140], [16,140], [14,141], [17,141], [15,142], [16,142], [219,60], [220,60], [218,61], [221,61], [219,62], [221,62], [220,63], [194,191], [195,191], [196,191], [193,192], [194,192], [195,192], [85,76], [86,76], [84,77], [87,77], [85,78], [87,78], [86,79], [178,124], [179,124], [177,125], [180,125], [178,126], [180,126], [179,127], [11,23], [12,23], [13,23], [64,3], [65,3], [63,4], [66,4], [64,5], [65,5], [39,180], [40,180], [38,181], [41,181], [39,182], [40,182], [174,164], [175,164], [173,165], [176,165], [174,166], [175,166], [132,174], [133,174], [131,175], [134,175], [132,176], [133,176]] },
|
||
{ id: 79, name: 'Схема №79', w: 236, h: 236, wrap: false, budget: 209,
|
||
zone: { x: 53, y: 53, w: 130, h: 130 },
|
||
cells: [[159,179], [160,179], [158,180], [161,180], [159,181], [160,181], [61,209], [62,209], [63,209], [15,94], [16,94], [14,95], [17,95], [15,96], [16,96], [165,182], [166,182], [167,182], [164,183], [165,183], [166,183], [207,82], [208,82], [206,83], [209,83], [207,84], [208,84], [136,191], [137,191], [135,192], [138,192], [136,193], [138,193], [137,194], [53,49], [54,49], [52,50], [55,50], [53,51], [55,51], [54,52], [146,96], [147,96], [145,97], [148,97], [146,98], [148,98], [147,99], [208,141], [209,141], [210,141], [207,142], [208,142], [209,142], [8,100], [9,100], [7,101], [10,101], [8,102], [9,102]] },
|
||
{ id: 80, name: 'Схема №80', w: 240, h: 240, wrap: false, budget: 212,
|
||
zone: { x: 0, y: 0, w: 152, h: 133 },
|
||
cells: [[32,145], [33,145], [34,145], [38,145], [39,145], [40,145], [30,147], [35,147], [37,147], [42,147], [30,148], [35,148], [37,148], [42,148], [30,149], [35,149], [37,149], [42,149], [32,150], [33,150], [34,150], [38,150], [39,150], [40,150], [32,152], [33,152], [34,152], [38,152], [39,152], [40,152], [30,153], [35,153], [37,153], [42,153], [30,154], [35,154], [37,154], [42,154], [30,155], [35,155], [37,155], [42,155], [32,157], [33,157], [34,157], [38,157], [39,157], [40,157], [216,87], [217,87], [218,87], [215,88], [216,88], [217,88], [40,197], [41,197], [42,197], [39,198], [40,198], [41,198], [91,88], [92,88], [93,88], [90,89], [91,89], [92,89], [106,2], [107,2], [108,2], [105,3], [106,3], [107,3], [97,206], [98,206], [99,206], [50,29], [51,29], [49,30], [52,30], [50,31], [52,31], [51,32], [88,171], [89,171], [90,171], [97,52], [98,52], [99,52], [96,53], [97,53], [98,53], [52,128], [53,128], [54,128], [39,8], [40,8], [41,8], [38,9], [39,9], [40,9]] },
|
||
{ id: 81, name: 'Схема №81', w: 242, h: 242, wrap: false, budget: 214,
|
||
zone: { x: 133, y: 165, w: 109, h: 77 },
|
||
cells: [[159,35], [160,35], [158,36], [161,36], [159,37], [161,37], [160,38], [13,128], [14,128], [12,129], [15,129], [13,130], [14,130], [177,164], [178,164], [179,164], [9,43], [10,43], [8,44], [11,44], [9,45], [11,45], [10,46], [172,32], [173,32], [171,33], [174,33], [172,34], [174,34], [173,35], [179,21], [180,21], [181,21], [178,22], [179,22], [180,22], [85,162], [86,162], [87,162], [84,163], [85,163], [86,163], [75,161], [76,161], [74,162], [77,162], [75,163], [76,163], [145,128], [146,128], [147,128], [34,184], [35,184], [33,185], [36,185], [34,186], [36,186], [35,187], [161,148], [162,148], [163,148], [160,149], [161,149], [162,149]] },
|
||
{ id: 82, name: 'Схема №82', w: 246, h: 246, wrap: false, budget: 217,
|
||
zone: { x: 107, y: 0, w: 51, h: 246 },
|
||
cells: [[104,103], [102,104], [104,104], [92,105], [93,105], [100,105], [101,105], [114,105], [115,105], [91,106], [95,106], [100,106], [101,106], [114,106], [115,106], [80,107], [81,107], [90,107], [96,107], [100,107], [101,107], [80,108], [81,108], [90,108], [94,108], [96,108], [97,108], [102,108], [104,108], [90,109], [96,109], [104,109], [91,110], [95,110], [92,111], [93,111], [174,143], [175,143], [176,143], [180,143], [181,143], [182,143], [172,145], [177,145], [179,145], [184,145], [172,146], [177,146], [179,146], [184,146], [172,147], [177,147], [179,147], [184,147], [174,148], [175,148], [176,148], [180,148], [181,148], [182,148], [174,150], [175,150], [176,150], [180,150], [181,150], [182,150], [172,151], [177,151], [179,151], [184,151], [172,152], [177,152], [179,152], [184,152], [172,153], [177,153], [179,153], [184,153], [174,155], [175,155], [176,155], [180,155], [181,155], [182,155], [226,127], [227,127], [228,127], [225,128], [226,128], [227,128], [130,207], [131,207], [129,208], [132,208], [130,209], [131,209], [211,64], [212,64], [213,64], [210,65], [211,65], [212,65], [39,138], [40,138], [41,138], [38,139], [39,139], [40,139], [112,3], [113,3], [114,3], [146,236], [147,236], [145,237], [148,237], [146,238], [147,238], [98,23], [99,23], [97,24], [100,24], [98,25], [99,25], [43,153], [44,153], [42,154], [45,154], [43,155], [44,155], [143,58], [144,58], [142,59], [145,59], [143,60], [144,60], [217,243], [218,243], [219,243], [216,244], [217,244], [218,244], [63,140], [64,140], [65,140]] },
|
||
{ id: 83, name: 'Схема №83', w: 248, h: 248, wrap: false, budget: 218,
|
||
zone: { x: 0, y: 118, w: 248, h: 59 },
|
||
cells: [[162,191], [163,191], [164,191], [168,191], [169,191], [170,191], [160,193], [165,193], [167,193], [172,193], [160,194], [165,194], [167,194], [172,194], [160,195], [165,195], [167,195], [172,195], [162,196], [163,196], [164,196], [168,196], [169,196], [170,196], [162,198], [163,198], [164,198], [168,198], [169,198], [170,198], [160,199], [165,199], [167,199], [172,199], [160,200], [165,200], [167,200], [172,200], [160,201], [165,201], [167,201], [172,201], [162,203], [163,203], [164,203], [168,203], [169,203], [170,203], [133,13], [134,13], [135,13], [125,48], [126,48], [124,49], [127,49], [125,50], [126,50], [204,89], [205,89], [206,89], [203,90], [204,90], [205,90], [117,30], [118,30], [116,31], [119,31], [117,32], [119,32], [118,33], [6,107], [7,107], [8,107], [5,108], [6,108], [7,108], [200,130], [201,130], [202,130], [199,131], [200,131], [201,131], [61,140], [62,140], [60,141], [63,141], [61,142], [63,142], [62,143], [23,212], [24,212], [25,212], [103,136], [104,136], [102,137], [105,137], [103,138], [104,138], [214,48], [215,48], [213,49], [216,49], [214,50], [215,50]] },
|
||
{ id: 84, name: 'Схема №84', w: 252, h: 252, wrap: false, budget: 222,
|
||
zone: { x: 11, y: 88, w: 112, h: 119 },
|
||
cells: [[150,229], [151,229], [149,230], [152,230], [150,231], [151,231], [30,116], [31,116], [29,117], [32,117], [30,118], [32,118], [31,119], [228,226], [229,226], [230,226], [227,227], [228,227], [229,227], [34,74], [35,74], [33,75], [36,75], [34,76], [36,76], [35,77], [13,186], [14,186], [15,186], [111,144], [112,144], [113,144], [160,187], [161,187], [162,187], [159,188], [160,188], [161,188], [12,41], [13,41], [11,42], [14,42], [12,43], [13,43], [30,17], [31,17], [29,18], [32,18], [30,19], [32,19], [31,20], [189,177], [190,177], [191,177], [188,178], [189,178], [190,178], [109,239], [110,239], [111,239]] },
|
||
{ id: 85, name: 'Схема №85', w: 254, h: 254, wrap: false, budget: 223,
|
||
zone: { x: 57, y: 57, w: 140, h: 140 },
|
||
cells: [[160,251], [161,251], [162,251], [159,252], [160,252], [161,252], [18,208], [19,208], [20,208], [17,209], [18,209], [19,209], [32,111], [33,111], [34,111], [31,112], [32,112], [33,112], [212,129], [213,129], [211,130], [214,130], [212,131], [213,131], [127,249], [128,249], [129,249], [126,250], [127,250], [128,250], [201,58], [202,58], [203,58], [248,21], [249,21], [250,21], [18,176], [19,176], [17,177], [20,177], [18,178], [20,178], [19,179], [195,107], [196,107], [194,108], [197,108], [195,109], [196,109], [31,127], [32,127], [33,127], [30,128], [31,128], [32,128], [203,35], [204,35], [202,36], [205,36], [203,37], [205,37], [204,38]] },
|
||
{ id: 86, name: 'Схема №86', w: 258, h: 258, wrap: false, budget: 226,
|
||
zone: { x: 0, y: 0, w: 258, h: 129 },
|
||
cells: [[230,184], [231,184], [232,184], [236,184], [237,184], [238,184], [228,186], [233,186], [235,186], [240,186], [228,187], [233,187], [235,187], [240,187], [228,188], [233,188], [235,188], [240,188], [230,189], [231,189], [232,189], [236,189], [237,189], [238,189], [230,191], [231,191], [232,191], [236,191], [237,191], [238,191], [228,192], [233,192], [235,192], [240,192], [228,193], [233,193], [235,193], [240,193], [228,194], [233,194], [235,194], [240,194], [230,196], [231,196], [232,196], [236,196], [237,196], [238,196], [56,165], [57,165], [55,166], [58,166], [56,167], [58,167], [57,168], [87,229], [88,229], [86,230], [89,230], [87,231], [88,231], [145,171], [146,171], [147,171], [144,172], [145,172], [146,172], [64,108], [65,108], [66,108], [63,109], [64,109], [65,109], [122,247], [123,247], [121,248], [124,248], [122,249], [124,249], [123,250], [26,210], [27,210], [28,210], [25,211], [26,211], [27,211], [124,40], [125,40], [126,40], [123,41], [124,41], [125,41], [175,197], [176,197], [177,197], [174,198], [175,198], [176,198], [114,17], [115,17], [113,18], [116,18], [114,19], [116,19], [115,20], [142,48], [143,48], [141,49], [144,49], [142,50], [144,50], [143,51], [68,188], [69,188], [70,188], [67,189], [68,189], [69,189], [86,161], [87,161], [88,161]] },
|
||
{ id: 87, name: 'Схема №87', w: 260, h: 260, wrap: false, budget: 228,
|
||
zone: { x: 58, y: 58, w: 143, h: 143 },
|
||
cells: [[198,52], [199,52], [200,52], [197,53], [198,53], [199,53], [188,79], [189,79], [190,79], [187,80], [188,80], [189,80], [96,144], [97,144], [98,144], [95,145], [96,145], [97,145], [256,55], [257,55], [255,56], [258,56], [256,57], [258,57], [257,58], [209,1], [210,1], [211,1], [208,2], [209,2], [210,2], [237,15], [238,15], [236,16], [239,16], [237,17], [238,17], [38,195], [39,195], [40,195], [37,196], [38,196], [39,196], [67,129], [68,129], [69,129], [66,130], [67,130], [68,130], [5,41], [6,41], [4,42], [7,42], [5,43], [7,43], [6,44], [62,136], [63,136], [61,137], [64,137], [62,138], [63,138], [121,59], [122,59], [123,59], [120,60], [121,60], [122,60]] },
|
||
{ id: 88, name: 'Схема №88', w: 264, h: 264, wrap: false, budget: 231,
|
||
zone: { x: 0, y: 0, w: 139, h: 128 },
|
||
cells: [[33,129], [34,129], [32,130], [35,130], [33,131], [35,131], [34,132], [207,24], [208,24], [206,25], [209,25], [207,26], [209,26], [208,27], [130,75], [131,75], [129,76], [132,76], [130,77], [131,77], [115,149], [116,149], [114,150], [117,150], [115,151], [117,151], [116,152], [241,11], [242,11], [240,12], [243,12], [241,13], [242,13], [60,201], [61,201], [59,202], [62,202], [60,203], [61,203], [133,128], [134,128], [135,128], [61,227], [62,227], [63,227], [234,18], [235,18], [233,19], [236,19], [234,20], [235,20], [214,259], [215,259], [213,260], [216,260], [214,261], [216,261], [215,262], [252,192], [253,192], [254,192], [251,193], [252,193], [253,193]] },
|
||
{ id: 89, name: 'Схема №89', w: 266, h: 266, wrap: false, budget: 233,
|
||
zone: { x: 144, y: 163, w: 122, h: 103 },
|
||
cells: [[122,238], [123,238], [124,238], [128,238], [129,238], [130,238], [120,240], [125,240], [127,240], [132,240], [120,241], [125,241], [127,241], [132,241], [120,242], [125,242], [127,242], [132,242], [122,243], [123,243], [124,243], [128,243], [129,243], [130,243], [122,245], [123,245], [124,245], [128,245], [129,245], [130,245], [120,246], [125,246], [127,246], [132,246], [120,247], [125,247], [127,247], [132,247], [120,248], [125,248], [127,248], [132,248], [122,250], [123,250], [124,250], [128,250], [129,250], [130,250], [25,48], [26,48], [27,48], [167,244], [168,244], [166,245], [169,245], [167,246], [168,246], [260,157], [261,157], [262,157], [259,158], [260,158], [261,158], [216,229], [217,229], [218,229], [215,230], [216,230], [217,230], [212,262], [213,262], [211,263], [214,263], [212,264], [213,264], [214,131], [215,131], [213,132], [216,132], [214,133], [215,133], [171,114], [172,114], [170,115], [173,115], [171,116], [173,116], [172,117], [160,184], [161,184], [162,184], [159,185], [160,185], [161,185], [159,206], [160,206], [161,206], [247,231], [248,231], [249,231], [246,232], [247,232], [248,232]] },
|
||
{ id: 90, name: 'Схема №90', w: 270, h: 270, wrap: false, budget: 236,
|
||
zone: { x: 110, y: 0, w: 68, h: 270 },
|
||
cells: [[91,64], [92,64], [93,64], [97,64], [98,64], [99,64], [89,66], [94,66], [96,66], [101,66], [89,67], [94,67], [96,67], [101,67], [89,68], [94,68], [96,68], [101,68], [91,69], [92,69], [93,69], [97,69], [98,69], [99,69], [91,71], [92,71], [93,71], [97,71], [98,71], [99,71], [89,72], [94,72], [96,72], [101,72], [89,73], [94,73], [96,73], [101,73], [89,74], [94,74], [96,74], [101,74], [91,76], [92,76], [93,76], [97,76], [98,76], [99,76], [196,253], [197,253], [195,254], [198,254], [196,255], [197,255], [143,84], [144,84], [142,85], [145,85], [143,86], [145,86], [144,87], [72,91], [73,91], [74,91], [235,17], [236,17], [237,17], [234,18], [235,18], [236,18], [8,20], [9,20], [7,21], [10,21], [8,22], [10,22], [9,23], [56,178], [57,178], [58,178], [55,179], [56,179], [57,179], [21,109], [22,109], [20,110], [23,110], [21,111], [22,111], [230,236], [231,236], [232,236], [46,13], [47,13], [45,14], [48,14], [46,15], [47,15], [138,213], [139,213], [137,214], [140,214], [138,215], [139,215], [49,190], [50,190], [51,190], [48,191], [49,191], [50,191], [188,25], [189,25], [190,25], [187,26], [188,26], [189,26], [229,88], [230,88], [228,89], [231,89], [229,90], [230,90]] },
|
||
{ id: 91, name: 'Схема №91', w: 272, h: 272, wrap: false, budget: 238,
|
||
zone: { x: 0, y: 184, w: 272, h: 70 },
|
||
cells: [[48,201], [49,201], [50,201], [47,202], [48,202], [49,202], [222,110], [223,110], [221,111], [224,111], [222,112], [223,112], [20,56], [21,56], [22,56], [19,57], [20,57], [21,57], [149,219], [150,219], [151,219], [148,220], [149,220], [150,220], [262,72], [263,72], [264,72], [261,73], [262,73], [263,73], [205,111], [206,111], [204,112], [207,112], [205,113], [206,113], [48,230], [49,230], [50,230], [47,231], [48,231], [49,231], [19,215], [20,215], [21,215], [19,154], [20,154], [18,155], [21,155], [19,156], [20,156], [113,262], [114,262], [112,263], [115,263], [113,264], [114,264], [160,104], [161,104], [162,104], [177,248], [178,248], [176,249], [179,249], [177,250], [178,250]] },
|
||
{ id: 92, name: 'Схема №92', w: 276, h: 276, wrap: false, budget: 241,
|
||
zone: { x: 41, y: 20, w: 190, h: 170 },
|
||
cells: [[201,2], [202,2], [203,2], [207,2], [208,2], [209,2], [199,4], [204,4], [206,4], [211,4], [199,5], [204,5], [206,5], [211,5], [199,6], [204,6], [206,6], [211,6], [201,7], [202,7], [203,7], [207,7], [208,7], [209,7], [201,9], [202,9], [203,9], [207,9], [208,9], [209,9], [199,10], [204,10], [206,10], [211,10], [199,11], [204,11], [206,11], [211,11], [199,12], [204,12], [206,12], [211,12], [201,14], [202,14], [203,14], [207,14], [208,14], [209,14], [162,264], [163,264], [164,264], [161,265], [162,265], [163,265], [72,28], [73,28], [74,28], [273,108], [274,108], [272,109], [275,109], [273,110], [274,110], [39,55], [40,55], [41,55], [20,265], [21,265], [19,266], [22,266], [20,267], [21,267], [133,161], [134,161], [132,162], [135,162], [133,163], [135,163], [134,164], [246,136], [247,136], [248,136], [245,137], [246,137], [247,137], [154,58], [155,58], [156,58], [153,59], [154,59], [155,59], [74,128], [75,128], [76,128], [27,189], [28,189], [29,189], [26,190], [27,190], [28,190], [131,192], [132,192], [130,193], [133,193], [131,194], [132,194]] },
|
||
{ id: 93, name: 'Схема №93', w: 278, h: 278, wrap: false, budget: 242,
|
||
zone: { x: 62, y: 62, w: 153, h: 153 },
|
||
cells: [[265,145], [266,145], [264,146], [267,146], [265,147], [266,147], [111,152], [112,152], [113,152], [110,153], [111,153], [112,153], [25,124], [26,124], [24,125], [27,125], [25,126], [26,126], [160,173], [161,173], [159,174], [162,174], [160,175], [161,175], [160,183], [161,183], [159,184], [162,184], [160,185], [161,185], [232,189], [233,189], [234,189], [161,85], [162,85], [160,86], [163,86], [161,87], [162,87], [19,160], [20,160], [21,160], [18,161], [19,161], [20,161], [110,252], [111,252], [109,253], [112,253], [110,254], [112,254], [111,255], [239,2], [240,2], [238,3], [241,3], [239,4], [240,4], [197,234], [198,234], [199,234], [196,235], [197,235], [198,235], [218,235], [219,235], [217,236], [220,236], [218,237], [219,237]] },
|
||
{ id: 94, name: 'Схема №94', w: 282, h: 282, wrap: false, budget: 246,
|
||
zone: { x: 0, y: 141, w: 282, h: 141 },
|
||
cells: [[26,225], [24,226], [26,226], [14,227], [15,227], [22,227], [23,227], [36,227], [37,227], [13,228], [17,228], [22,228], [23,228], [36,228], [37,228], [2,229], [3,229], [12,229], [18,229], [22,229], [23,229], [2,230], [3,230], [12,230], [16,230], [18,230], [19,230], [24,230], [26,230], [12,231], [18,231], [26,231], [13,232], [17,232], [14,233], [15,233], [172,154], [173,154], [174,154], [178,154], [179,154], [180,154], [170,156], [175,156], [177,156], [182,156], [170,157], [175,157], [177,157], [182,157], [170,158], [175,158], [177,158], [182,158], [172,159], [173,159], [174,159], [178,159], [179,159], [180,159], [172,161], [173,161], [174,161], [178,161], [179,161], [180,161], [170,162], [175,162], [177,162], [182,162], [170,163], [175,163], [177,163], [182,163], [170,164], [175,164], [177,164], [182,164], [172,166], [173,166], [174,166], [178,166], [179,166], [180,166], [253,264], [254,264], [252,265], [255,265], [253,266], [254,266], [203,204], [204,204], [205,204], [193,112], [194,112], [192,113], [195,113], [193,114], [194,114], [125,51], [126,51], [124,52], [127,52], [125,53], [127,53], [126,54], [19,123], [20,123], [21,123], [18,124], [19,124], [20,124], [89,226], [90,226], [91,226], [88,227], [89,227], [90,227], [258,70], [259,70], [260,70], [79,98], [80,98], [78,99], [81,99], [79,100], [80,100], [231,251], [232,251], [233,251], [230,252], [231,252], [232,252], [90,235], [91,235], [89,236], [92,236], [90,237], [92,237], [91,238], [91,217], [92,217], [93,217], [204,118], [205,118], [203,119], [206,119], [204,120], [206,120], [205,121]] },
|
||
{ id: 95, name: 'Схема №95', w: 284, h: 284, wrap: false, budget: 247,
|
||
zone: { x: 64, y: 64, w: 156, h: 156 },
|
||
cells: [[66,229], [67,229], [68,229], [72,229], [73,229], [74,229], [64,231], [69,231], [71,231], [76,231], [64,232], [69,232], [71,232], [76,232], [64,233], [69,233], [71,233], [76,233], [66,234], [67,234], [68,234], [72,234], [73,234], [74,234], [66,236], [67,236], [68,236], [72,236], [73,236], [74,236], [64,237], [69,237], [71,237], [76,237], [64,238], [69,238], [71,238], [76,238], [64,239], [69,239], [71,239], [76,239], [66,241], [67,241], [68,241], [72,241], [73,241], [74,241], [28,9], [29,9], [30,9], [235,43], [236,43], [237,43], [116,59], [117,59], [115,60], [118,60], [116,61], [117,61], [8,160], [9,160], [10,160], [7,161], [8,161], [9,161], [30,251], [31,251], [32,251], [142,256], [143,256], [141,257], [144,257], [142,258], [143,258], [41,138], [42,138], [43,138], [218,243], [219,243], [217,244], [220,244], [218,245], [219,245], [158,181], [159,181], [157,182], [160,182], [158,183], [160,183], [159,184], [228,81], [229,81], [230,81], [227,82], [228,82], [229,82], [74,151], [75,151], [76,151], [73,152], [74,152], [75,152]] },
|
||
{ id: 96, name: 'Схема №96', w: 288, h: 288, wrap: false, budget: 250,
|
||
zone: { x: 0, y: 0, w: 132, h: 124 },
|
||
cells: [[184,214], [185,214], [183,215], [186,215], [184,216], [186,216], [185,217], [208,42], [209,42], [210,42], [207,43], [208,43], [209,43], [200,113], [201,113], [199,114], [202,114], [200,115], [202,115], [201,116], [152,11], [153,11], [154,11], [151,12], [152,12], [153,12], [234,260], [235,260], [233,261], [236,261], [234,262], [235,262], [174,265], [175,265], [173,266], [176,266], [174,267], [175,267], [7,90], [8,90], [6,91], [9,91], [7,92], [9,92], [8,93], [202,38], [203,38], [204,38], [201,39], [202,39], [203,39], [48,269], [49,269], [50,269], [47,270], [48,270], [49,270], [116,211], [117,211], [115,212], [118,212], [116,213], [117,213], [89,3], [90,3], [91,3], [236,198], [237,198], [235,199], [238,199], [236,200], [237,200]] },
|
||
{ id: 97, name: 'Схема №97', w: 292, h: 292, wrap: false, budget: 254,
|
||
zone: { x: 160, y: 109, w: 132, h: 183 },
|
||
cells: [[85,106], [86,106], [84,107], [87,107], [85,108], [87,108], [86,109], [77,189], [78,189], [79,189], [137,145], [138,145], [139,145], [136,146], [137,146], [138,146], [269,20], [270,20], [268,21], [271,21], [269,22], [270,22], [99,167], [100,167], [101,167], [98,168], [99,168], [100,168], [234,190], [235,190], [233,191], [236,191], [234,192], [235,192], [256,166], [257,166], [255,167], [258,167], [256,168], [257,168], [113,262], [114,262], [112,263], [115,263], [113,264], [115,264], [114,265], [276,287], [277,287], [278,287], [275,288], [276,288], [277,288], [21,162], [22,162], [23,162], [20,163], [21,163], [22,163], [126,135], [127,135], [128,135], [125,136], [126,136], [127,136], [139,160], [140,160], [141,160], [138,161], [139,161], [140,161]] },
|
||
{ id: 98, name: 'Схема №98', w: 294, h: 294, wrap: false, budget: 255,
|
||
zone: { x: 101, y: 0, w: 59, h: 294 },
|
||
cells: [[85,19], [86,19], [87,19], [91,19], [92,19], [93,19], [83,21], [88,21], [90,21], [95,21], [83,22], [88,22], [90,22], [95,22], [83,23], [88,23], [90,23], [95,23], [85,24], [86,24], [87,24], [91,24], [92,24], [93,24], [85,26], [86,26], [87,26], [91,26], [92,26], [93,26], [83,27], [88,27], [90,27], [95,27], [83,28], [88,28], [90,28], [95,28], [83,29], [88,29], [90,29], [95,29], [85,31], [86,31], [87,31], [91,31], [92,31], [93,31], [11,51], [12,51], [13,51], [213,7], [214,7], [212,8], [215,8], [213,9], [214,9], [252,279], [253,279], [254,279], [251,280], [252,280], [253,280], [106,160], [107,160], [105,161], [108,161], [106,162], [107,162], [149,230], [150,230], [151,230], [148,231], [149,231], [150,231], [146,2], [147,2], [148,2], [260,201], [261,201], [259,202], [262,202], [260,203], [262,203], [261,204], [241,91], [242,91], [243,91], [226,23], [227,23], [228,23], [225,24], [226,24], [227,24], [140,180], [141,180], [139,181], [142,181], [140,182], [142,182], [141,183], [52,250], [53,250], [54,250], [197,101], [198,101], [199,101], [196,102], [197,102], [198,102], [63,51], [64,51], [65,51]] },
|
||
{ id: 99, name: 'Схема №99', w: 298, h: 298, wrap: false, budget: 258,
|
||
zone: { x: 0, y: 164, w: 298, h: 47 },
|
||
cells: [[237,36], [238,36], [239,36], [236,37], [237,37], [238,37], [161,184], [162,184], [163,184], [160,185], [161,185], [162,185], [295,285], [296,285], [297,285], [294,286], [295,286], [296,286], [291,99], [292,99], [293,99], [290,100], [291,100], [292,100], [96,20], [97,20], [95,21], [98,21], [96,22], [97,22], [274,289], [275,289], [273,290], [276,290], [274,291], [275,291], [9,151], [10,151], [8,152], [11,152], [9,153], [10,153], [185,53], [186,53], [184,54], [187,54], [185,55], [186,55], [160,43], [161,43], [162,43], [159,44], [160,44], [161,44], [196,178], [197,178], [195,179], [198,179], [196,180], [197,180], [266,242], [267,242], [268,242], [265,243], [266,243], [267,243], [92,176], [93,176], [91,177], [94,177], [92,178], [94,178], [93,179]] },
|
||
{ id: 100, name: 'Схема №100', w: 300, h: 300, wrap: false, budget: 260,
|
||
zone: { x: 106, y: 39, w: 184, h: 105 },
|
||
cells: [[17,97], [18,97], [16,98], [19,98], [17,99], [18,99], [265,63], [266,63], [264,64], [267,64], [265,65], [267,65], [266,66], [215,160], [216,160], [217,160], [132,99], [133,99], [134,99], [244,62], [245,62], [246,62], [74,136], [75,136], [76,136], [73,137], [74,137], [75,137], [14,181], [15,181], [13,182], [16,182], [14,183], [15,183], [283,289], [284,289], [285,289], [282,290], [283,290], [284,290], [51,51], [52,51], [50,52], [53,52], [51,53], [53,53], [52,54], [222,78], [223,78], [221,79], [224,79], [222,80], [223,80], [61,223], [62,223], [60,224], [63,224], [61,225], [62,225], [291,153], [292,153], [293,153], [290,154], [291,154], [292,154], [111,25], [112,25], [113,25]] },
|
||
];
|
||
|
||
// Разбор ASCII-паттерна в offsets {x,y} (лениво, с кэшем на объекте: превью каждый
|
||
// кадр должно быть дёшево). p._cells — массив offsets, p._w/p._h — размеры bbox.
|
||
function patternCells(p) {
|
||
if (!p) return [];
|
||
if (p._cells) return p._cells;
|
||
const out = [];
|
||
let w = 0;
|
||
for (let y = 0; y < p.cells.length; y++) {
|
||
const line = p.cells[y];
|
||
if (line.length > w) w = line.length;
|
||
for (let x = 0; x < line.length; x++) if (line[x] === '1') out.push({ x: x, y: y });
|
||
}
|
||
p._cells = out;
|
||
p._w = w;
|
||
p._h = p.cells.length;
|
||
return out;
|
||
}
|
||
// Паттерн по id (или null, если id неизвестен).
|
||
function findPattern(id) {
|
||
for (let i = 0; i < PATTERNS.length; i++) if (PATTERNS[i].id === id) return PATTERNS[i];
|
||
return null;
|
||
}
|
||
|
||
// Поворот набора offsets на 90° по часовой стрелке: внутри bbox (w×h) клетка
|
||
// (x,y) → (h-1-y, x). Размеры bbox меняются местами (w×h → h×w), а offsets снова
|
||
// нормализованы к левому верхнему углу (якорь штампа не меняется).
|
||
function rotate90(cells) {
|
||
let h = 0;
|
||
for (let i = 0; i < cells.length; i++) if (cells[i].y + 1 > h) h = cells[i].y + 1;
|
||
const out = new Array(cells.length);
|
||
for (let i = 0; i < cells.length; i++) out[i] = { x: h - 1 - cells[i].y, y: cells[i].x };
|
||
return out;
|
||
}
|
||
// Offsets активного паттерна с учётом поворота stampRot (0..3). Базовый разбор
|
||
// кэширует patternCells (p._cells), повёрнутый — на объекте паттерна (p._rot1..3):
|
||
// поворот колесом не пересобирает offsets каждый кадр.
|
||
function stampCells() {
|
||
if (!stamp) return [];
|
||
if (!stampRot) return patternCells(stamp);
|
||
const key = '_rot' + stampRot;
|
||
if (!stamp[key]) {
|
||
let out = patternCells(stamp);
|
||
for (let r = 0; r < stampRot; r++) out = rotate90(out);
|
||
stamp[key] = out;
|
||
}
|
||
return stamp[key];
|
||
}
|
||
|
||
// ==== ХОЛСТ И РАЗМЕРЫ ====
|
||
const canvas = document.getElementById('game');
|
||
const ctx = canvas.getContext('2d');
|
||
var viewW = 0; // логическая ширина холста (CSS-пиксели)
|
||
var viewH = 0; // логическая высота холста
|
||
var dpr = 1; // текущий коэффициент плотности пикселей
|
||
// Геометрия доски: клетки квадратные, поле может быть прямоугольным (fieldW×fieldH).
|
||
// board.w/board.h — пиксельный размер доски; board.size — legacy-алиас ширины
|
||
// (для квадратных полей w == h; читается тестами/отладкой); board.cell — сторона клетки.
|
||
var board = { x: 0, y: 0, w: 0, h: 0, size: 0, cell: 0 };
|
||
|
||
// Пересчёт размеров холста под окно с учётом DPR (кап 2 — как в ../fishing-game).
|
||
function resize() {
|
||
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||
viewW = window.innerWidth;
|
||
viewH = window.innerHeight;
|
||
canvas.width = Math.round(viewW * dpr);
|
||
canvas.height = Math.round(viewH * dpr);
|
||
canvas.style.width = viewW + 'px';
|
||
canvas.style.height = viewH + 'px';
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
computeBoard();
|
||
}
|
||
|
||
// Вычисление положения/размера доски. Клетки квадратные, поле может быть прямоугольным
|
||
// (fieldW×fieldH), поэтому доска — прямоугольник board.w×board.h. Две раскладки:
|
||
// • боковая (ландшафт, body.side): доска во всю высоту, прижата влево, управление
|
||
// в свободной зоне справа (ширина -> CSS-переменная --side-w);
|
||
// • обычная: доска по центру, с резервами сверху (HUD) и снизу (тулбар+действия).
|
||
// layoutSide — активна ли боковая раскладка (читается тестами/отладкой).
|
||
var layoutSide = false;
|
||
var SIDE_MIN = 210; // минимальная ширина боковой панели: иначе обычная раскладка
|
||
function computeBoard() {
|
||
const pad = 10;
|
||
const fullH = viewH - pad * 2; // доска во всю высоту
|
||
// Боковая раскладка: доска занимает всю высоту. Её ширина зависит от пропорций поля,
|
||
// поэтому проверяем, что справа реально остаётся место под панель управления.
|
||
const sideBoardW = (fullH / fieldH) * fieldW;
|
||
layoutSide = viewW > viewH && (viewW - sideBoardW - pad * 3) >= SIDE_MIN;
|
||
if (layoutSide) {
|
||
board.cell = fullH / fieldH;
|
||
board.w = sideBoardW;
|
||
board.h = fullH;
|
||
const sideW = Math.max(SIDE_MIN, viewW - board.w - pad * 3);
|
||
document.documentElement.style.setProperty('--side-w', sideW + 'px');
|
||
board.x = pad; // доска прижата влево, во всю высоту
|
||
board.y = Math.round((viewH - board.h) / 2);
|
||
} else {
|
||
// bottom резервирует место под тулбар (с кнопками поворота/тапа он может занимать
|
||
// две строки) и нижнюю панель действий — чтобы доска их не перекрывала.
|
||
const top = 64, bottom = 210;
|
||
const availW = viewW - pad * 2;
|
||
const availH = viewH - top - bottom;
|
||
// Одна клетка на обе оси (клетки квадратные): вписываем поле по меньшей стороне,
|
||
// но не даём доске выродиться (мин. 60px по ограничивающей оси).
|
||
const cell = Math.min(availW / fieldW, Math.max(60, availH) / fieldH);
|
||
board.cell = cell;
|
||
board.w = cell * fieldW;
|
||
board.h = cell * fieldH;
|
||
board.x = Math.round((viewW - board.w) / 2);
|
||
board.y = Math.round(top + Math.max(0, (availH - board.h) / 2));
|
||
}
|
||
board.size = board.w; // legacy-алиас (квадратное поле: w == h)
|
||
document.body.classList.toggle('side', layoutSide);
|
||
}
|
||
|
||
// ==== СОСТОЯНИЕ ИГРЫ ====
|
||
var state = 'MENU'; // текущее состояние FSM
|
||
var mode = 'menu'; // режим: menu/place/edit/handoff/run/pause/over
|
||
var turn = 1; // чей сейчас ход (1 или 2)
|
||
var tool = TOOLS[0]; // активный инструмент
|
||
var stamp = null; // активный паттерн-штамп (PATTERNS[i]) или null; транзиентно (не в сейв)
|
||
var stampRot = 0; // поворот активного штампа: 0..3 (90° по часовой за шаг); транзиентно
|
||
// Режим «постановка по отпусканию» (тап для сенсорных экранов): при нажатии — только
|
||
// превью под пальцем, постановка/стирание — в момент отпускания. Drag-рисование линии
|
||
// отключено. Транзиентный, как speedMult/stamp (в сейв не пишется). pressing — удержание.
|
||
var tapPlace = false;
|
||
var pressing = false;
|
||
var winner = null; // '1' | '2' | 'draw' (после финала)
|
||
var reason = null; // 'extinct' | 'manual' | 'stalemate' (причина финала)
|
||
var fieldW = DEFAULT_FIELD;
|
||
var fieldH = DEFAULT_FIELD;
|
||
var field = new Uint8Array(fieldW * fieldH); // 0 = мёртвая, 1 = P1, 2 = P2
|
||
var next = new Uint8Array(fieldW * fieldH); // буфер следующего поколения
|
||
var turnStartField = null; // снимок поля на начало текущей фазы рисования («Сброс»)
|
||
var wrap = false; // «Замкнутая карта» (torus): соседи за краем берутся с другой стороны
|
||
// Бюджет клеток на партию: total — лимит на игрока (настройка), usedN — потрачено (инкрементально).
|
||
var budget = { total: DEFAULT_BUDGET, used1: 0, used2: 0 };
|
||
// Снимок бюджета на начало текущего хода (для «Сброса» рядом с turnStartField).
|
||
var turnStartBudget = { used1: 0, used2: 0 };
|
||
// Прерванная партия, отложенная в меню. Кнопка «В меню»/⚙️ не теряет игру:
|
||
// снимок целиком (размер/wrap/поле/бюджет) живёт здесь, пока игрок в меню,
|
||
// и «Продолжить партию» возвращает ровно в ту же партию.
|
||
var suspended = null;
|
||
// Открытый диалог подтверждения рискованного действия: null | 'new' | 'end'.
|
||
// Ни одна кнопка не завершает/не теряет партию без такого подтверждения.
|
||
var confirmAction = null;
|
||
// Был ли RUNNING до открытия подтверждения завершения (чтобы вернуть симуляцию
|
||
// при «Отмене»; на время диалога симуляция ставится на паузу).
|
||
var confirmResume = false;
|
||
// --- Режим «Кампания» ---
|
||
// campaign — идёт ли уровень кампании; levelIndex — индекс уровня (0..) или -1;
|
||
// zoneRect — прямоугольник зоны рисования игрока на уровне или null (обычная игра);
|
||
// campaignProgress — прогресс (сколько уровней разблокировано) — хранится в сейве;
|
||
// pendingLevel — уровень, ожидающий подтверждения в диалоге (confirmAction='campaign').
|
||
var campaign = false;
|
||
var levelIndex = -1;
|
||
var zoneRect = null;
|
||
var campaignProgress = { unlocked: 1 };
|
||
var pendingLevel = null;
|
||
// --- Пользовательские настройки (из меню) vs «активная игра» (D1) ---
|
||
// Кампания применяет размер/wrap/бюджет УРОВНЯ к активной игре (fieldW/fieldH/wrap/
|
||
// budget.total), но НЕ трогает эти значения. Они живут отдельно и восстанавливаются
|
||
// при выходе из кампании (toMenu/showLevelSelect) и в меню-контролах syncDom.
|
||
// persist() пишет активную игру в fieldW/fieldH/wrap/budget (для резюма) и отдельно —
|
||
// пользовательские настройки в menuFieldW/menuFieldH/menuWrap/menuBudget.
|
||
var menuFieldW = DEFAULT_FIELD;
|
||
var menuFieldH = DEFAULT_FIELD;
|
||
var menuWrap = false;
|
||
var menuBudget = DEFAULT_BUDGET;
|
||
// Нужно ли пересобрать offscreen-слой клеток при следующем draw (см. секцию ОТРИСОВКА).
|
||
// Ставится при любом изменении поля/маски/размера; в статичных фазах draw — только drawImage.
|
||
var renderDirty = true;
|
||
|
||
// --- Стабильность поля (гейт дорисовки) ---
|
||
// Дорисовка разрешена ТОЛЬКО когда поле перестало меняться: step не меняет поле
|
||
// (фиксированная точка, период 1) ИЛИ новое поколение повторило одно из последних
|
||
// STABLE_WINDOW (короткий цикл — ловит блинкеры/осцилляторы). Отслеживается во
|
||
// время RUNNING: при обнаружении цикличности симуляция САМА встаёт на паузу и
|
||
// открывает дорисовку. Если поле не застывает (глайдеры/рост/хаос) — дорисовки
|
||
// нет, партию завершают вручную. Сравнение по хешу поля (FNV-1a): коллизия
|
||
// 32-битного хеша даёт ложное срабатывание с вероятностью ~2^-32 — пренебрежимо.
|
||
var fieldStable = false; // поле на паузе устойчиво (дорисовка разрешена)
|
||
var recentHashes = []; // скользящее окно хешей последних поколений (RUNNING)
|
||
const STABLE_WINDOW = 1024; // максимальный распознаваемый период цикла
|
||
function resetStability() { recentHashes.length = 0; fieldStable = false; }
|
||
// Засеять окно хешем текущего поколения (старт симуляции/тест-форс RUNNING):
|
||
// фиксированная точка ловится на 1-м тике, блинкер — на 2-м.
|
||
function seedStability() { recentHashes.length = 0; recentHashes.push(hashField(field)); fieldStable = false; }
|
||
|
||
// Списки состояний для быстрых проверок.
|
||
const DRAW_STATES = { PLACE_P1: 1, PLACE_P2: 1, EDIT_P1: 1, EDIT_P2: 1 };
|
||
// Handoff — только в первичной расстановке: HANDOFF_P1_P2 (передать вслепую игроку 2)
|
||
// и HANDOFF_P2_START (расстановка завершена: вернуться к P1 или начать симуляцию).
|
||
// В дорисовке (EDIT_*) оба видят поле, передача устройства не нужна: «Готов» возвращает
|
||
// в паузу, откуда игрок сам выбирает — дорисовать второму или запустить симуляцию.
|
||
const HANDOFF_STATES = { HANDOFF_P1_P2: 1, HANDOFF_P2_START: 1 };
|
||
function isDrawState(s) { return !!DRAW_STATES[s]; }
|
||
function isHandoffState(s) { return !!HANDOFF_STATES[s]; }
|
||
|
||
// Состояния с незавершённой партией — подлежат резюму после reload.
|
||
const RESUMABLE_STATES = { PLACE_P1: 1, PLACE_P2: 1, EDIT_P1: 1, EDIT_P2: 1,
|
||
RUNNING: 1, PAUSED: 1, HANDOFF_P1_P2: 1, HANDOFF_P2_START: 1 };
|
||
function isResumable(s) { return !!RESUMABLE_STATES[s]; }
|
||
|
||
// Производный описатель фазы (menu/place/edit/handoff/run/pause/over) из state.
|
||
function modeForState(s) {
|
||
if (s === 'LEVEL_SELECT' || s === 'TWO_SETUP') return 'menu'; // экраны выбора/настройки
|
||
if (s === 'PLACE_P1' || s === 'PLACE_P2') return 'place';
|
||
if (s === 'EDIT_P1' || s === 'EDIT_P2') return 'edit';
|
||
if (isHandoffState(s)) return 'handoff';
|
||
if (s === 'RUNNING') return 'run';
|
||
if (s === 'PAUSED') return 'pause';
|
||
if (s === 'GAME_OVER') return 'over';
|
||
return 'menu';
|
||
}
|
||
|
||
// ==== МОДЕЛЬ ПОЛЯ (чистая, без DOM) ====
|
||
// Таблицы индексов соседей: колонки (colL/colR) и строки (rowU/rowD).
|
||
// При wrap=false содержат координату x±1 / y±1 (за краем -> -1 или W/H, отсекается
|
||
// guard'ом), при wrap=true — координату с другой стороны (torus). Пересобираются
|
||
// при смене размера/опции — в горячем цикле НЕТ деления по модулю.
|
||
// NB: таблицы индексов строятся по ГЛОБАЛЬНОМУ wrap; передавай явный useWrap только
|
||
// согласованно с setWrap, иначе обход (ветка wrap=true) будет неверным.
|
||
var colL = new Int16Array(0), colR = new Int16Array(0);
|
||
var rowU = new Int16Array(0), rowD = new Int16Array(0);
|
||
function rebuildNeighborTables() {
|
||
colL = new Int16Array(fieldW);
|
||
colR = new Int16Array(fieldW);
|
||
rowU = new Int16Array(fieldH);
|
||
rowD = new Int16Array(fieldH);
|
||
for (let x = 0; x < fieldW; x++) {
|
||
colL[x] = wrap ? (x - 1 + fieldW) % fieldW : x - 1;
|
||
colR[x] = wrap ? (x + 1) % fieldW : x + 1;
|
||
}
|
||
for (let y = 0; y < fieldH; y++) {
|
||
rowU[y] = wrap ? (y - 1 + fieldH) % fieldH : y - 1;
|
||
rowD[y] = wrap ? (y + 1) % fieldH : y + 1;
|
||
}
|
||
}
|
||
// Установка опции «Замкнутая карта»: глобал + пересборка таблиц соседей + рендер.
|
||
function setWrap(b) {
|
||
wrap = !!b;
|
||
menuWrap = !!b; // пользовательская настройка (см. menuFieldW)
|
||
rebuildNeighborTables();
|
||
resetStability(); // смена правила соседей — окно хешей прежнего правила недействительно
|
||
renderDirty = true; // смена заворота может поменять видимую маску/поле к следующему draw
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Число живых соседей Мура (8). useWrap опционален: по умолчанию — глобал wrap.
|
||
// wrap=false — границы ЖЁСТКИЕ (соседи за краем не считаются, семантика как раньше).
|
||
// wrap=true — torus (противоположная сторона через таблицы индексов).
|
||
function liveNeighbors(f, x, y, useWrap) {
|
||
const w = (useWrap === undefined) ? wrap : useWrap;
|
||
let n = 0;
|
||
if (w) {
|
||
const l = colL[x], r = colR[x];
|
||
const u = rowU[y] * fieldW, d = rowD[y] * fieldW, c = y * fieldW;
|
||
if (f[u + l] !== 0) n++;
|
||
if (f[u + x] !== 0) n++;
|
||
if (f[u + r] !== 0) n++;
|
||
if (f[c + l] !== 0) n++;
|
||
if (f[c + r] !== 0) n++;
|
||
if (f[d + l] !== 0) n++;
|
||
if (f[d + x] !== 0) n++;
|
||
if (f[d + r] !== 0) n++;
|
||
} else {
|
||
for (let dy = -1; dy <= 1; dy++) {
|
||
const ny = y + dy;
|
||
if (ny < 0 || ny >= fieldH) continue;
|
||
for (let dx = -1; dx <= 1; dx++) {
|
||
if (!dx && !dy) continue;
|
||
const nx = x + dx;
|
||
if (nx < 0 || nx >= fieldW) continue;
|
||
if (f[ny * fieldW + nx] !== 0) n++;
|
||
}
|
||
}
|
||
}
|
||
return n;
|
||
}
|
||
|
||
// Цвет большинства живых цветов; при неоднозначности — наименьший id (tie-break).
|
||
// Принимает любое число аргументов (для рождения передаём 8 соседей, нули игнорируются).
|
||
function majorityColor() {
|
||
const counts = {};
|
||
for (let i = 0; i < arguments.length; i++) {
|
||
const c = arguments[i];
|
||
if (c > 0) counts[c] = (counts[c] || 0) + 1;
|
||
}
|
||
let best = 0, bestN = 0;
|
||
for (const k in counts) {
|
||
const c = +k, n = counts[k];
|
||
if (n > bestN || (n === bestN && c < best)) { best = c; bestN = n; }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
// Один тик автомата: читает src, пишет dst; src НЕ мутируется. B3/S23.
|
||
// useWrap опционален: по умолчанию — глобал wrap. При wrap=false ветка идентична
|
||
// прежней (жёсткие границы) — паритет с прототипом сохраняется.
|
||
// NB: ветка wrap=true использует таблицы индексов, построенные по ГЛОБАЛУ wrap;
|
||
// явный useWrap передавай только согласованно с setWrap, иначе обход неверен.
|
||
function step(src, dst, useWrap) {
|
||
const w = (useWrap === undefined) ? wrap : useWrap;
|
||
if (w) {
|
||
for (let y = 0; y < fieldH; y++) {
|
||
const c = y * fieldW;
|
||
const u = rowU[y] * fieldW, d = rowD[y] * fieldW;
|
||
for (let x = 0; x < fieldW; x++) {
|
||
const i = c + x;
|
||
const cur = src[i];
|
||
const l = colL[x], r = colR[x];
|
||
const vl = src[u + l], vu = src[u + x], vr = src[u + r];
|
||
const cl = src[c + l], cr = src[c + r];
|
||
const dl = src[d + l], du = src[d + x], dr = src[d + r];
|
||
const n = (vl !== 0 ? 1 : 0) + (vu !== 0 ? 1 : 0) + (vr !== 0 ? 1 : 0) +
|
||
(cl !== 0 ? 1 : 0) + (cr !== 0 ? 1 : 0) +
|
||
(dl !== 0 ? 1 : 0) + (du !== 0 ? 1 : 0) + (dr !== 0 ? 1 : 0);
|
||
if (cur !== 0) {
|
||
dst[i] = (n === 2 || n === 3) ? cur : 0;
|
||
} else if (n === 3) {
|
||
dst[i] = majorityColor(vl, vu, vr, cl, cr, dl, du, dr);
|
||
} else {
|
||
dst[i] = 0;
|
||
}
|
||
}
|
||
}
|
||
return dst;
|
||
}
|
||
for (let y = 0; y < fieldH; y++) {
|
||
const row = y * fieldW;
|
||
for (let x = 0; x < fieldW; x++) {
|
||
const i = row + x;
|
||
const cur = src[i];
|
||
const n = liveNeighbors(src, x, y, false);
|
||
if (cur !== 0) {
|
||
dst[i] = (n === 2 || n === 3) ? cur : 0; // выживание при 2–3
|
||
} else if (n === 3) {
|
||
// Рождение: цвет большинства из 3 живых соседей.
|
||
let k = 0, a = 0, b = 0, c = 0;
|
||
for (let dy = -1; dy <= 1; dy++) {
|
||
const ny = y + dy;
|
||
if (ny < 0 || ny >= fieldH) continue;
|
||
for (let dx = -1; dx <= 1; dx++) {
|
||
if (!dx && !dy) continue;
|
||
const nx = x + dx;
|
||
if (nx < 0 || nx >= fieldW) continue;
|
||
const v = src[ny * fieldW + nx];
|
||
if (v !== 0) { if (k === 0) a = v; else if (k === 1) b = v; else c = v; k++; }
|
||
}
|
||
}
|
||
dst[i] = majorityColor(a, b, c);
|
||
} else {
|
||
dst[i] = 0;
|
||
}
|
||
}
|
||
}
|
||
return dst;
|
||
}
|
||
|
||
// n тиков подряд, синхронно, по полному полю (с учётом буферов).
|
||
// useWrap опционален: по умолчанию — глобал wrap.
|
||
function stepN(n, useWrap) {
|
||
const w = (useWrap === undefined) ? wrap : useWrap;
|
||
let src = field, dst = next;
|
||
for (let i = 0; i < n; i++) {
|
||
step(src, dst, w);
|
||
const t = src; src = dst; dst = t;
|
||
}
|
||
field = src; next = dst;
|
||
renderDirty = true; // поле изменилось — пересобрать offscreen-слой к следующему draw
|
||
return field;
|
||
}
|
||
|
||
// Штамп инструмента size×size с центрированием и клампом к границам поля.
|
||
// ЧИСТО ГЕОМЕТРИЧЕСКИЙ примитив: без зоны и бюджета (на него завязаны тесты);
|
||
// игровое действие (зона + бюджет) — в paintTool ниже.
|
||
function applyBrush(f, x, y, t, player) {
|
||
t = t || tool;
|
||
const size = t.size;
|
||
const half = size >> 1;
|
||
for (let dy = -half; dy < size - half; dy++) {
|
||
for (let dx = -half; dx < size - half; dx++) {
|
||
const nx = x + dx, ny = y + dy;
|
||
if (nx < 0 || ny < 0 || nx >= fieldW || ny >= fieldH) continue;
|
||
f[ny * fieldW + nx] = t.erase ? 0 : player;
|
||
}
|
||
}
|
||
return f;
|
||
}
|
||
|
||
// Зона рисования: P1 — левая половина, P2 — правая. Целочисленно (ок и для нечётной ширины).
|
||
function inZone(x, player) {
|
||
return player === 1 ? (2 * x < fieldW) : (2 * x >= fieldW);
|
||
}
|
||
// Граница зон (первый x правой половины) + предикат — для UI/рендера (R2-2).
|
||
function zoneSplit() {
|
||
return { boundary: Math.ceil(fieldW / 2), inZone: inZone };
|
||
}
|
||
// Зона рисования с учётом кампании: в кампании — прямоугольник zoneRect (любой
|
||
// размер/позиция, вплоть до всего поля), иначе — половина (inZone). inZone/zoneSplit
|
||
// НЕ меняются (контракт тестов): игровые места ходят через inZoneAt(x,y,player).
|
||
function inZoneAt(x, y, player) {
|
||
if (campaign) {
|
||
if (!zoneRect) return false;
|
||
return x >= zoneRect.x && x < zoneRect.x + zoneRect.w &&
|
||
y >= zoneRect.y && y < zoneRect.y + zoneRect.h;
|
||
}
|
||
return inZone(x, player);
|
||
}
|
||
|
||
// Остаток бюджета игрока (1|2).
|
||
function remaining(player) {
|
||
const used = player === 2 ? budget.used2 : budget.used1;
|
||
return budget.total - used;
|
||
}
|
||
// Установка лимита бюджета (настройка; применяется в newGame). Значение — целое ≥ 0.
|
||
function setBudgetTotal(n) {
|
||
n = Math.floor(Number(n));
|
||
if (!Number.isFinite(n) || n < 0) return;
|
||
budget.total = n;
|
||
menuBudget = n; // пользовательская настройка (см. menuFieldW)
|
||
persist();
|
||
}
|
||
|
||
// ИГРОВОЕ действие инструмента в клетке (x,y): учитывает фазу, зону и бюджет.
|
||
// Возвращает число изменённых клеток. Единственная точка мутации budget.used*.
|
||
// - краска: новая/чужая клетка при remaining>0 -> player (+1 бюджет); своя — без траты;
|
||
// при частичной нехватке ставим min(new, remaining) в детерминированном порядке;
|
||
// - ластик: снимает ТОЛЬКО правку текущего хода (field===player и turnStartField!==player),
|
||
// восстанавливая turnStartField[i] и возвращая бюджет; чужое/старшее — не трогает.
|
||
function paintTool(x, y) {
|
||
if (!isDrawState(state)) return 0;
|
||
if (!inZoneAt(x, y, turn)) return 0;
|
||
const t = paintOverride || tool, player = turn, size = t.size, half = size >> 1;
|
||
let changed = 0;
|
||
if (t.erase) {
|
||
for (let dy = -half; dy < size - half; dy++) {
|
||
const ny = y + dy;
|
||
if (ny < 0 || ny >= fieldH) continue;
|
||
for (let dx = -half; dx < size - half; dx++) {
|
||
const nx = x + dx;
|
||
if (nx < 0 || nx >= fieldW) continue;
|
||
if (!inZoneAt(nx, ny, player)) continue;
|
||
const i = ny * fieldW + nx;
|
||
if (campaign && field[i] === CAMPAIGN_ENEMY) continue; // врага стирать нельзя
|
||
const ts = turnStartField ? turnStartField[i] : 0;
|
||
if (field[i] === player && ts !== player) {
|
||
field[i] = ts;
|
||
if (player === 2) budget.used2--; else budget.used1--;
|
||
changed++;
|
||
}
|
||
}
|
||
}
|
||
} else if (stamp) {
|
||
// Штамп готового паттерна: якорь — левый верхний угол bbox в клетке (x,y).
|
||
// Зона/бюджет — как у краски: вне зоны не ставим, чужая/пустая тратит 1,
|
||
// своя не тратит, при нехватке ставим min(new, remaining) в порядке клеток.
|
||
let used = player === 2 ? budget.used2 : budget.used1;
|
||
let rem = budget.total - used;
|
||
const cells = stampCells(); // с учётом поворота колесом (stampRot)
|
||
for (let k = 0; k < cells.length; k++) {
|
||
const nx = x + cells[k].x, ny = y + cells[k].y;
|
||
if (nx < 0 || ny < 0 || nx >= fieldW || ny >= fieldH) continue; // кламп по доске
|
||
if (!inZoneAt(nx, ny, player)) continue; // обрезка по зоне
|
||
const i = ny * fieldW + nx;
|
||
if (campaign && field[i] === CAMPAIGN_ENEMY) continue; // врага не перезаписываем
|
||
if (field[i] === player) continue; // своя клетка — бюджет не тратит
|
||
if (rem <= 0) continue; // бюджет исчерпан — новые не пишем
|
||
field[i] = player;
|
||
used++; rem--;
|
||
changed++;
|
||
}
|
||
if (player === 2) budget.used2 = used; else budget.used1 = used;
|
||
} else {
|
||
let used = player === 2 ? budget.used2 : budget.used1;
|
||
let rem = budget.total - used;
|
||
for (let dy = -half; dy < size - half; dy++) {
|
||
const ny = y + dy;
|
||
if (ny < 0 || ny >= fieldH) continue;
|
||
for (let dx = -half; dx < size - half; dx++) {
|
||
const nx = x + dx;
|
||
if (nx < 0 || nx >= fieldW) continue;
|
||
if (!inZoneAt(nx, ny, player)) continue;
|
||
const i = ny * fieldW + nx;
|
||
if (campaign && field[i] === CAMPAIGN_ENEMY) continue; // врага не перезаписываем
|
||
if (field[i] === player) continue; // своя клетка — бюджет не тратит
|
||
if (rem <= 0) continue; // бюджет исчерпан — новые не пишем
|
||
field[i] = player;
|
||
used++; rem--;
|
||
changed++;
|
||
}
|
||
}
|
||
if (player === 2) budget.used2 = used; else budget.used1 = used;
|
||
}
|
||
if (changed) renderDirty = true;
|
||
return changed;
|
||
}
|
||
|
||
// Счётчики живых клеток по цветам.
|
||
function countByPlayer(f) {
|
||
f = f || field;
|
||
let p1 = 0, p2 = 0;
|
||
for (let i = 0; i < f.length; i++) {
|
||
const v = f[i];
|
||
if (v === 1) p1++; else if (v === 2) p2++;
|
||
}
|
||
return { p1: p1, p2: p2 };
|
||
}
|
||
function getScores() { return countByPlayer(field); }
|
||
|
||
// Вымирание: на доске остался не более одного цвета.
|
||
function isExtinct(f) {
|
||
f = f || field;
|
||
let a = false, b = false;
|
||
for (let i = 0; i < f.length; i++) {
|
||
if (f[i] === 1) a = true; else if (f[i] === 2) b = true;
|
||
if (a && b) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Детерминированный хеш поля (FNV-1a, 32 бита).
|
||
function hashField(f) {
|
||
const arr = f || field;
|
||
let h = 0x811c9dc5 >>> 0;
|
||
for (let i = 0; i < arr.length; i++) {
|
||
h ^= arr[i];
|
||
h = Math.imul(h, 0x01000193) >>> 0;
|
||
}
|
||
return h >>> 0;
|
||
}
|
||
|
||
// Маска видимых цветов по состоянию (скрытие — только визуальное).
|
||
function visibleMask() {
|
||
if (campaign) return [1, 2]; // в кампании скрытия нет: враг виден всегда
|
||
switch (state) {
|
||
case 'PLACE_P1': return [1];
|
||
case 'PLACE_P2': return [2];
|
||
case 'HANDOFF_P1_P2': case 'HANDOFF_P2_START': return [];
|
||
default: return [1, 2]; // RUNNING/PAUSED/EDIT_*/GAME_OVER/MENU — всё видно
|
||
}
|
||
}
|
||
|
||
// ==== FSM / ЛОГИКА ====
|
||
// Начать фазу рисования: выставить состояние/ход и снять снимок поля для «Сброса».
|
||
function beginDraw(st, tn) {
|
||
state = st;
|
||
turn = tn;
|
||
mode = modeForState(st);
|
||
turnStartField = field.slice();
|
||
turnStartBudget = { used1: budget.used1, used2: budget.used2 };
|
||
renderDirty = true; // смена маски/фазы — пересобрать слой клеток
|
||
}
|
||
|
||
// Обнулить в массиве клетки зоны игрока (для «чистого» снимка первичной расстановки).
|
||
function clearZone(arr, player) {
|
||
if (campaign) return; // в кампании «чистый» снимок зоны не нужен (нельзя терять врага)
|
||
for (let y = 0; y < fieldH; y++) {
|
||
for (let x = 0; x < fieldW; x++) {
|
||
if (inZoneAt(x, y, player)) arr[y * fieldW + x] = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Вход в фазу первичной расстановки с «чистым» снимком начала хода для текущего
|
||
// игрока: в снимке обнуляется его зона (прошлая расстановка этого игрока), клетки
|
||
// соперника сохраняются. Так «Сброс»/ластик откатывают именно расстановку игрока,
|
||
// а возврат от P2 к P1 (и повторная передача) не теряет чужую половину и корректно
|
||
// возвращает бюджет (текущий игрок начинает «с чистого листа»).
|
||
function enterPlace(st, tn) {
|
||
beginDraw(st, tn);
|
||
clearZone(turnStartField, tn);
|
||
turnStartBudget = { used1: tn === 1 ? 0 : budget.used1, used2: tn === 2 ? 0 : budget.used2 };
|
||
}
|
||
|
||
// Новая партия: чистое поле, обнуление бюджета, ход игрока 1, первичная расстановка.
|
||
function newGame() {
|
||
field.fill(0);
|
||
next.fill(0);
|
||
winner = null;
|
||
reason = null;
|
||
suspended = null; // новая партия вытесняет отложенную
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
campaign = false; // обычная hot-seat партия — не кампания
|
||
levelIndex = -1;
|
||
zoneRect = null;
|
||
pendingLevel = null;
|
||
budget.used1 = 0;
|
||
budget.used2 = 0;
|
||
speedMult = 1; // новая партия стартует с обычной скорости
|
||
resetStability(); // чистое поле — устойчивость ещё не оценивалась
|
||
beginDraw('PLACE_P1', 1);
|
||
simAcc = 0;
|
||
last = performance.now();
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// «Готов» в фазе рисования.
|
||
function commitTurn() {
|
||
if (campaign) {
|
||
// Кампания: без передачи устройства и без P2. «Готов» и из расстановки, и из
|
||
// дорисовки сразу запускает симуляцию — дорисовка замкнута в цикл «нарисовал →
|
||
// сразу запустил», отдельной кнопки «Старт симуляции» не требуется.
|
||
if (state === 'PLACE_P1' || state === 'EDIT_P1') startRunning();
|
||
} else {
|
||
switch (state) {
|
||
case 'PLACE_P1': state = 'HANDOFF_P1_P2'; break;
|
||
case 'PLACE_P2': state = 'HANDOFF_P2_START'; break; // выбор: вернуться к P1 или старт
|
||
case 'EDIT_P1': startPaused(); break; // дорисовка → пауза: дальше выбор (P2/старт)
|
||
case 'EDIT_P2': startPaused(); break;
|
||
}
|
||
}
|
||
mode = modeForState(state);
|
||
renderDirty = true; // HANDOFF меняет маску видимости
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Кнопка на оверлее передачи/подтверждения.
|
||
function handoffNext() {
|
||
if (state === 'HANDOFF_P1_P2') enterPlace('PLACE_P2', 2);
|
||
else if (state === 'HANDOFF_P2_START') startRunning();
|
||
mode = modeForState(state);
|
||
renderDirty = true; // HANDOFF меняет маску видимости
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Возврат от P2 к игроку 1 после расстановки P2 (HANDOFF_P2_START): P1 правит свою
|
||
// половину, расстановка P2 (в правой половине) сохраняется и снова скрыта — слепая
|
||
// расстановка не нарушается. Затем P1 снова передаёт устройство P2.
|
||
function handoffBack() {
|
||
if (state !== 'HANDOFF_P2_START') return;
|
||
enterPlace('PLACE_P1', 1);
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Вернуться в паузу после хода дорисовки (между ходами снимок не нужен).
|
||
function startPaused() {
|
||
state = 'PAUSED';
|
||
mode = modeForState(state);
|
||
turnStartField = null;
|
||
turnStartBudget = { used1: 0, used2: 0 };
|
||
renderDirty = true;
|
||
simAcc = 0;
|
||
last = performance.now();
|
||
lastPersistAt = performance.now();
|
||
}
|
||
|
||
// Запуск симуляции (сбрасываем аккумулятор, чтобы шаг не «догонял»).
|
||
function startRunning() {
|
||
state = 'RUNNING';
|
||
mode = modeForState(state);
|
||
turnStartField = null; // в симуляции снимок хода не нужен (и не пишем его в сейв)
|
||
turnStartBudget = { used1: 0, used2: 0 }; // симметрично: снимок бюджета тоже не храним
|
||
seedStability(); // начинаем следить за устойчивостью поля с текущего поколения
|
||
renderDirty = true;
|
||
simAcc = 0;
|
||
last = performance.now();
|
||
lastPersistAt = performance.now();
|
||
}
|
||
|
||
function pauseGame() {
|
||
if (state === 'RUNNING') { state = 'PAUSED'; mode = modeForState(state); renderDirty = true; syncDom(); persist(); }
|
||
}
|
||
function resumeGame() {
|
||
if (state === 'PAUSED') { startRunning(); syncDom(); persist(); }
|
||
}
|
||
// Ряд скорости: 4 кнопки 1×/2×/4×/8×. Выбор множителя транзиентный — в сейв не пишем.
|
||
function setSpeed(m) {
|
||
speedMult = (m === 2 || m === 4 || m === 8) ? m : 1;
|
||
syncDom();
|
||
}
|
||
// Дорисовка на паузе. Разрешена ТОЛЬКО когда поле устойчиво (fieldStable) — т.е.
|
||
// симуляция сама остановилась, достигнув фиксированной точки/короткого цикла; на
|
||
// «живом» поле (глайдеры/хаос) дорисовки нет. Игрок сам выбирает, кто дорисовывает
|
||
// (кнопки «Дорисовать красным»/«Дорисовать зелёным»), и отдельно запускает симуляцию.
|
||
// Оба видят поле целиком (скрытие — только в первичной расстановке). Без аргумента — P1.
|
||
function startEdit(player) {
|
||
if (state !== 'PAUSED' || !fieldStable) return;
|
||
if (campaign && player === 2) return; // в кампании игрок только один (P1)
|
||
const p = player === 2 ? 2 : 1;
|
||
beginDraw(p === 2 ? 'EDIT_P2' : 'EDIT_P1', p);
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Копия прямоугольника зоны уровня (или null) — чтобы не мутировать CAMPAIGN_LEVELS.
|
||
function campaignZoneFor(idx) {
|
||
const lv = CAMPAIGN_LEVELS[idx];
|
||
if (!lv || !lv.zone) return null;
|
||
return { x: lv.zone.x, y: lv.zone.y, w: lv.zone.w, h: lv.zone.h };
|
||
}
|
||
// Заполнить поле клетками врага уровня (не через setFieldASCII — без побочек:
|
||
// снимок начала хода делает beginDraw, а «чистить» зону в кампании нельзя).
|
||
function buildCampaignField(lv) {
|
||
fieldW = lv.w;
|
||
fieldH = lv.h;
|
||
field = new Uint8Array(fieldW * fieldH);
|
||
next = new Uint8Array(fieldW * fieldH);
|
||
const cells = lv.cells || [];
|
||
for (let i = 0; i < cells.length; i++) {
|
||
const x = cells[i][0], y = cells[i][1];
|
||
if (x < 0 || y < 0 || x >= fieldW || y >= fieldH) continue; // защита от выхода за поле
|
||
field[y * fieldW + x] = CAMPAIGN_ENEMY;
|
||
}
|
||
}
|
||
// Запустить уровень кампании idx: враг на поле, зона-прямоугольник, свой бюджет,
|
||
// сразу фаза расстановки P1 (без передачи устройства). Враг остаётся в снимке хода
|
||
// (clearZone/enterPlace НЕ зовём — иначе враг сотрётся из turnStartField).
|
||
function startLevel(idx) {
|
||
const lv = CAMPAIGN_LEVELS[idx];
|
||
if (!lv) return;
|
||
campaign = true;
|
||
levelIndex = idx;
|
||
zoneRect = campaignZoneFor(idx);
|
||
wrap = !!lv.wrap;
|
||
buildCampaignField(lv);
|
||
budget.total = lv.budget;
|
||
budget.used1 = 0;
|
||
budget.used2 = 0;
|
||
turnStartBudget = { used1: 0, used2: 0 };
|
||
winner = null;
|
||
reason = null;
|
||
suspended = null;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
pendingLevel = null;
|
||
speedMult = 1;
|
||
stamp = null; stampRot = 0; wheelAccum = 0;
|
||
paintOverride = null; painting = false; pressing = false; lastCell = null;
|
||
resetStability();
|
||
rebuildNeighborTables();
|
||
computeBoard();
|
||
beginDraw('PLACE_P1', CAMPAIGN_PLAYER);
|
||
simAcc = 0;
|
||
last = performance.now();
|
||
lastPersistAt = performance.now();
|
||
syncDom();
|
||
persist();
|
||
}
|
||
// Вернуть «активную игру» к пользовательским настройкам меню: размер/wrap/бюджет.
|
||
// Вызывается при выходе из кампании (toMenu/showLevelSelect) — уровень мог подменить
|
||
// эти значения, и обычная партия обязана стартовать с прежних настроек пользователя (D1).
|
||
function applyMenuSettings() {
|
||
fieldW = menuFieldW;
|
||
fieldH = menuFieldH;
|
||
wrap = menuWrap;
|
||
budget.total = menuBudget;
|
||
field = new Uint8Array(fieldW * fieldH);
|
||
next = new Uint8Array(fieldW * fieldH);
|
||
turnStartField = null;
|
||
budget.used1 = 0;
|
||
budget.used2 = 0;
|
||
turnStartBudget = { used1: 0, used2: 0 };
|
||
rebuildNeighborTables();
|
||
resetStability();
|
||
renderDirty = true;
|
||
computeBoard();
|
||
}
|
||
// Экран «Игра на двоих» (состояние TWO_SETUP, не резюмируется): справка и настройки
|
||
// поля перед стартом обычной партии. Настройки относятся только к hot-seat-партии,
|
||
// поэтому с «сплэша» MENU их не видно — они живут здесь.
|
||
function showTwoSetup() {
|
||
campaign = false;
|
||
levelIndex = -1;
|
||
zoneRect = null;
|
||
pendingLevel = null;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
applyMenuSettings(); // активная игра возвращается к настройкам пользователя
|
||
state = 'TWO_SETUP';
|
||
mode = 'menu';
|
||
renderDirty = true;
|
||
syncDom();
|
||
persist();
|
||
}
|
||
// Экран выбора уровня кампании (отдельное состояние LEVEL_SELECT, не резюмируется).
|
||
function showLevelSelect() {
|
||
campaign = false;
|
||
levelIndex = -1;
|
||
zoneRect = null;
|
||
pendingLevel = null;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
applyMenuSettings(); // активная игра возвращается к настройкам пользователя
|
||
state = 'LEVEL_SELECT';
|
||
mode = 'menu';
|
||
renderDirty = true;
|
||
syncDom();
|
||
persist();
|
||
}
|
||
// Выбор уровня из списка: если уровень открыт — спросить подтверждение (текущая/отложенная
|
||
// партия будет потеряна), затем startLevel. Закрытый уровень игнорируем.
|
||
function requestCampaignLevel(idx) {
|
||
if (!Number.isInteger(idx) || idx < 0 || idx >= CAMPAIGN_LEVELS.length) return;
|
||
if (idx >= campaignProgress.unlocked) return;
|
||
pendingLevel = idx;
|
||
confirmAction = 'campaign';
|
||
confirmResume = false;
|
||
syncDom();
|
||
}
|
||
|
||
// Итог партии: победитель по числу живых клеток.
|
||
function computeWinner() {
|
||
if (campaign) {
|
||
// В кампании исход определяется причиной финала: победа — уничтожен враг,
|
||
// поражение — исчерпан бюджет при живом враге.
|
||
winner = reason === 'win' ? '1' : (reason === 'defeat' ? '2' : 'draw');
|
||
return winner;
|
||
}
|
||
const s = countByPlayer(field);
|
||
winner = s.p1 > s.p2 ? '1' : (s.p2 > s.p1 ? '2' : 'draw');
|
||
return winner;
|
||
}
|
||
function finish(rc) {
|
||
reason = rc;
|
||
computeWinner();
|
||
// Победа в кампании разблокирует следующий уровень (unlocked — 1-based число
|
||
// открытых уровней), с клампом по числу уровней.
|
||
if (campaign && rc === 'win' && levelIndex >= 0) {
|
||
const nextUnlocked = Math.min(CAMPAIGN_LEVELS.length, levelIndex + 2);
|
||
if (nextUnlocked > campaignProgress.unlocked) campaignProgress.unlocked = nextUnlocked;
|
||
}
|
||
state = 'GAME_OVER';
|
||
mode = modeForState(state);
|
||
renderDirty = true; // финал меняет маску видимости
|
||
syncDom();
|
||
persist();
|
||
}
|
||
// Ручное завершение партии (без диалога). Прямой вызов оставлен для тестов;
|
||
// кнопки «Завершить партию» идут через requestEndGame() с подтверждением.
|
||
function endGame() {
|
||
if (state === 'RUNNING' || state === 'PAUSED') finish('manual');
|
||
}
|
||
// Кнопка «Завершить партию»: сначала подтверждение (партию нельзя терять одним кликом).
|
||
// На время диалога симуляция ставится на паузу, чтобы поле не жило за оверлеем.
|
||
function requestEndGame() {
|
||
if (state !== 'RUNNING' && state !== 'PAUSED') return;
|
||
confirmResume = (state === 'RUNNING');
|
||
if (state === 'RUNNING') pauseGame();
|
||
confirmAction = 'end';
|
||
syncDom();
|
||
}
|
||
// Проверка вымирания (вызывается после каждого шага симуляции).
|
||
// Как только у игрока не осталось клеток его цвета — партия окончена: бюджет
|
||
// на дорисовку финал больше не отодвигает, а устойчивость поля не требуется.
|
||
function checkEnd() {
|
||
if (state !== 'RUNNING' && state !== 'PAUSED') return;
|
||
if (campaign) {
|
||
// Победа — не осталось клеток ВРАГА (цвет 2). Имеет приоритет над поражением.
|
||
// Вымирание игрока (0 клеток цвета 1) финалом НЕ является: пока есть бюджет,
|
||
// можно продолжать. Поражение ловится отдельно (checkStalemate на авто-паузе).
|
||
if (countByPlayer(field).p2 === 0) { finish('win'); return; }
|
||
return;
|
||
}
|
||
if (!isExtinct(field)) return;
|
||
finish('extinct');
|
||
}
|
||
// Тупик на стабильной паузе: отстающий игрок исчерпал бюджет и не может ни
|
||
// поставить, ни стереть ничего (ластик снимает только правку текущего хода),
|
||
// а поле устойчиво — значит исход зафиксирован, партию можно завершать.
|
||
// При равенстве клеток завершаем, только если бюджет исчерпан у ОБОИХ (иначе
|
||
// один из них ещё может разорвать ничью в свою пользу).
|
||
function checkStalemate() {
|
||
if (state !== 'PAUSED' || !fieldStable) return false;
|
||
if (campaign) {
|
||
// Поражение: поле стабильно, у игрока исчерпан бюджет, а враг ещё жив.
|
||
// Фиксируется ТОЛЬКО здесь (в ветке авто-паузы tick), как и stalemate.
|
||
if (remaining(1) <= 0 && countByPlayer(field).p2 > 0) { finish('defeat'); return true; }
|
||
return false;
|
||
}
|
||
const s = countByPlayer(field);
|
||
const r1 = remaining(1), r2 = remaining(2);
|
||
if (s.p1 === s.p2) {
|
||
if (r1 <= 0 && r2 <= 0) { finish('stalemate'); return true; }
|
||
return false;
|
||
}
|
||
const trailing = s.p1 < s.p2 ? 1 : 2;
|
||
if (remaining(trailing) <= 0) { finish('stalemate'); return true; }
|
||
return false;
|
||
}
|
||
|
||
// «Сброс»: только в фазах рисования — вернуть поле И бюджет к снимку начала хода.
|
||
function resetGame() {
|
||
if (isDrawState(state) && turnStartField) {
|
||
field.set(turnStartField);
|
||
budget.used1 = turnStartBudget.used1;
|
||
budget.used2 = turnStartBudget.used2;
|
||
renderDirty = true;
|
||
syncDom();
|
||
persist();
|
||
}
|
||
}
|
||
|
||
// ==== ПЕРСИСТЕНТНОСТЬ (localStorage, ключ SAVE_KEY) ====
|
||
// Формат сейва v2 (см. AGENTS.md «Форма сейва»):
|
||
// v — версия формата (для миграций; текущая 2);
|
||
// fieldW/fieldH — размер поля (настройка);
|
||
// wrap — «Замкнутая карта» (настройка);
|
||
// tool — id активного инструмента (настройка);
|
||
// stepsPerSec — скорость симуляции (настройка);
|
||
// budget — { total, used1, used2, start1, start2 };
|
||
// state/turn — состояние FSM и чей ход (резюм партии);
|
||
// reason/winner — финал (reason='extinct'|'manual'|'stalemate', winner='1'|'2'|'draw');
|
||
// field — поле ЦЕЛИКОМ в base64 (1 байт/клетка), только при резюме;
|
||
// turnStartField — снимок начала хода для «Сброса» (base64 | null). Зона не хранится
|
||
// (производна от fieldW).
|
||
function freshSave() {
|
||
return {
|
||
v: SAVE_VERSION,
|
||
fieldW: DEFAULT_FIELD, fieldH: DEFAULT_FIELD,
|
||
wrap: false,
|
||
// Пользовательские настройки меню (отдельно от активной игры; см. menuFieldW).
|
||
menuFieldW: DEFAULT_FIELD, menuFieldH: DEFAULT_FIELD, menuWrap: false, menuBudget: DEFAULT_BUDGET,
|
||
theme: 'dark',
|
||
tool: TOOLS[0].id, stepsPerSec: STEPS_PER_SEC,
|
||
budget: { total: DEFAULT_BUDGET, used1: 0, used2: 0, start1: 0, start2: 0 },
|
||
state: 'MENU', turn: 1,
|
||
reason: null, winner: null,
|
||
stable: false, // устойчивость поля на паузе (гейт дорисовки)
|
||
field: null, turnStartField: null,
|
||
suspended: null, // отложенная в меню партия (для кнопки «Продолжить партию»)
|
||
campaign: false, // идёт ли уровень кампании
|
||
levelIndex: null, // индекс уровня кампании (0..) или null
|
||
progress: { unlocked: 1 } // прогресс кампании: сколько уровней открыто
|
||
};
|
||
}
|
||
var save = freshSave();
|
||
|
||
// Нормализация блока бюджета сейва (защита от битых/частичных данных).
|
||
function normalizeBudget(b) {
|
||
const out = { total: DEFAULT_BUDGET, used1: 0, used2: 0, start1: 0, start2: 0 };
|
||
if (b && typeof b === 'object') {
|
||
if (Number.isFinite(b.total) && b.total >= 0) out.total = Math.floor(b.total);
|
||
if (Number.isFinite(b.used1) && b.used1 >= 0) out.used1 = Math.floor(b.used1);
|
||
if (Number.isFinite(b.used2) && b.used2 >= 0) out.used2 = Math.floor(b.used2);
|
||
if (Number.isFinite(b.start1) && b.start1 >= 0) out.start1 = Math.floor(b.start1);
|
||
if (Number.isFinite(b.start2) && b.start2 >= 0) out.start2 = Math.floor(b.start2);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Нормализация прогресса кампании: unlocked — число открытых уровней, клампится в [1, N].
|
||
function normalizeProgress(p) {
|
||
const out = { unlocked: 1 };
|
||
if (p && typeof p === 'object') {
|
||
let u = Math.floor(Number(p.unlocked));
|
||
if (!Number.isFinite(u) || u < 1) u = 1;
|
||
if (u > CAMPAIGN_LEVELS.length) u = CAMPAIGN_LEVELS.length;
|
||
out.unlocked = u;
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Инструмент по id (или null, если id неизвестен).
|
||
function findTool(id) {
|
||
for (let i = 0; i < TOOLS.length; i++) if (TOOLS[i].id === id) return TOOLS[i];
|
||
return null;
|
||
}
|
||
// Допустимый размер поля в сейве (не путать с FIELD_SIZES — это валидация выбора в меню).
|
||
function isValidSize(n) { return Number.isInteger(n) && n >= 1 && n <= 1000; }
|
||
|
||
// Поле (Uint8Array, 0/1/2) <-> base64: компактно и без spread-лимитов на больших массивах.
|
||
function bytesToB64(u8) {
|
||
let s = '';
|
||
for (let i = 0; i < u8.length; i += 0x8000) {
|
||
s += String.fromCharCode.apply(null, u8.subarray(i, i + 0x8000));
|
||
}
|
||
return btoa(s);
|
||
}
|
||
function b64ToBytes(b64) {
|
||
const bin = atob(b64);
|
||
const u8 = new Uint8Array(bin.length);
|
||
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
||
return u8;
|
||
}
|
||
|
||
// Нормализация отложенной партии из сейва: валидный резюмируемый снимок или null.
|
||
// Защита от битых/частичных данных — при любой проблеме отложенной партии нет.
|
||
function normalizeSuspended(raw) {
|
||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
||
if (!isResumable(raw.state)) return null;
|
||
if (!isValidSize(raw.fieldW) || !isValidSize(raw.fieldH)) return null;
|
||
if (typeof raw.field !== 'string') return null;
|
||
let f = null;
|
||
try { f = b64ToBytes(raw.field); } catch (e) { return null; }
|
||
if (!f || f.length !== raw.fieldW * raw.fieldH) return null;
|
||
let ts = null;
|
||
try { ts = typeof raw.turnStartField === 'string' ? b64ToBytes(raw.turnStartField) : null; } catch (e) { ts = null; }
|
||
return {
|
||
state: raw.state,
|
||
turn: raw.turn === 2 ? 2 : 1,
|
||
fieldW: raw.fieldW,
|
||
fieldH: raw.fieldH,
|
||
wrap: !!raw.wrap,
|
||
field: f,
|
||
turnStartField: (ts && ts.length === raw.fieldW * raw.fieldH) ? ts : null,
|
||
stable: !!raw.stable,
|
||
budget: normalizeBudget(raw.budget),
|
||
campaign: !!raw.campaign,
|
||
levelIndex: (Number.isInteger(raw.levelIndex) && raw.levelIndex >= 0 &&
|
||
raw.levelIndex < CAMPAIGN_LEVELS.length) ? raw.levelIndex : null
|
||
};
|
||
}
|
||
|
||
// Чтение life-save-v1 с миграцией. Возвращает нормализованный объект сейва,
|
||
// либо null при отсутствии/битых данных (тогда старт в MENU).
|
||
function loadSave() {
|
||
let raw = null;
|
||
try { raw = localStorage.getItem(SAVE_KEY); } catch (e) { return null; }
|
||
if (!raw) return null;
|
||
let obj;
|
||
try { obj = JSON.parse(raw); } catch (e) { return null; }
|
||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null;
|
||
|
||
// -- миграции формата (образец для будущих версий) --
|
||
// Старые сейвы обязаны оставаться читаемыми; нечитаемые — null (старт в MENU).
|
||
if (typeof obj.v !== 'number') obj.v = 1; // сейв без версии трактуем как v1
|
||
if (obj.v > SAVE_VERSION) return null; // формат новее — не рискуем
|
||
if (obj.v < SAVE_VERSION) {
|
||
// M1 (v1→v2): добавлены опция «Замкнутая карта» (дефолт выкл.) и бюджет клеток.
|
||
if (obj.v < 2) {
|
||
if (obj.wrap === undefined) obj.wrap = false;
|
||
if (!obj.budget) obj.budget = { total: DEFAULT_BUDGET, used1: 0, used2: 0, start1: 0, start2: 0 };
|
||
obj.v = 2;
|
||
}
|
||
}
|
||
|
||
// -- нормализация (защита от частичных/битых сейвов) --
|
||
const nW = isValidSize(obj.fieldW) ? obj.fieldW : DEFAULT_FIELD;
|
||
const nH = isValidSize(obj.fieldH) ? obj.fieldH : nW; // поле квадратное: fieldH дефолтится в fieldW
|
||
const nBudget = normalizeBudget(obj.budget);
|
||
const s = {
|
||
v: SAVE_VERSION,
|
||
fieldW: nW,
|
||
fieldH: nH,
|
||
wrap: !!obj.wrap,
|
||
// Пользовательские настройки меню: старые сейвы не знают этих полей — берём активные
|
||
// (в обычном потоке они совпадают), бюджет — из budget.total.
|
||
menuFieldW: isValidSize(obj.menuFieldW) ? obj.menuFieldW : nW,
|
||
menuFieldH: isValidSize(obj.menuFieldH) ? obj.menuFieldH : nH,
|
||
menuWrap: (obj.menuWrap === undefined) ? !!obj.wrap : !!obj.menuWrap,
|
||
menuBudget: (Number.isFinite(obj.menuBudget) && obj.menuBudget >= 0) ? Math.floor(obj.menuBudget) : nBudget.total,
|
||
theme: (obj.theme === 'light' || obj.theme === 'dark') ? obj.theme : 'dark',
|
||
tool: findTool(obj.tool) ? obj.tool : TOOLS[0].id,
|
||
stepsPerSec: (typeof obj.stepsPerSec === 'number' && obj.stepsPerSec > 0) ? obj.stepsPerSec : 8,
|
||
budget: nBudget,
|
||
state: typeof obj.state === 'string' ? obj.state : 'MENU',
|
||
turn: obj.turn === 2 ? 2 : 1,
|
||
reason: (obj.reason === 'extinct' || obj.reason === 'manual' || obj.reason === 'stalemate' ||
|
||
obj.reason === 'win' || obj.reason === 'defeat') ? obj.reason : null,
|
||
winner: (obj.winner === '1' || obj.winner === '2' || obj.winner === 'draw') ? obj.winner : null,
|
||
stable: !!obj.stable,
|
||
field: typeof obj.field === 'string' ? obj.field : null,
|
||
turnStartField: typeof obj.turnStartField === 'string' ? obj.turnStartField : null,
|
||
suspended: normalizeSuspended(obj.suspended),
|
||
campaign: !!obj.campaign,
|
||
levelIndex: (Number.isInteger(obj.levelIndex) && obj.levelIndex >= 0 &&
|
||
obj.levelIndex < CAMPAIGN_LEVELS.length) ? obj.levelIndex : null,
|
||
progress: normalizeProgress(obj.progress)
|
||
};
|
||
// Резюм возможен только для игровых состояний; финал/MENU/выбор уровня — это
|
||
// настройки + чистый старт (кампания при этом не продолжается).
|
||
if (!isResumable(s.state)) {
|
||
s.state = 'MENU';
|
||
s.field = null;
|
||
s.turnStartField = null;
|
||
s.campaign = false;
|
||
s.levelIndex = null;
|
||
}
|
||
return s;
|
||
}
|
||
|
||
// Запись сейва: настройки всегда + резюм партии в игровых состояниях.
|
||
// Вызывается на значимых переходах (FSM, инструмент, размер, пауза/ход), не каждый кадр.
|
||
function persist() {
|
||
save.v = SAVE_VERSION;
|
||
save.fieldW = fieldW; // активная игра (нужна для резюма; в кампании — размер уровня)
|
||
save.fieldH = fieldH;
|
||
save.wrap = wrap;
|
||
// Пользовательские настройки меню (кампания их не перезаписывает) — D1.
|
||
save.menuFieldW = menuFieldW;
|
||
save.menuFieldH = menuFieldH;
|
||
save.menuWrap = menuWrap;
|
||
save.menuBudget = menuBudget;
|
||
save.theme = theme;
|
||
save.tool = tool ? tool.id : TOOLS[0].id;
|
||
save.stepsPerSec = STEPS_PER_SEC;
|
||
save.state = state;
|
||
save.turn = turn;
|
||
save.reason = reason;
|
||
save.winner = winner;
|
||
save.stable = fieldStable;
|
||
save.campaign = campaign;
|
||
save.levelIndex = campaign ? levelIndex : null;
|
||
save.progress = { unlocked: campaignProgress.unlocked };
|
||
save.budget = {
|
||
total: budget.total, used1: budget.used1, used2: budget.used2,
|
||
start1: turnStartBudget.used1, start2: turnStartBudget.used2
|
||
};
|
||
if (isResumable(state) && field && field.length === fieldW * fieldH) {
|
||
try {
|
||
save.field = bytesToB64(field);
|
||
save.turnStartField = turnStartField ? bytesToB64(turnStartField) : null;
|
||
} catch (e) { save.field = null; save.turnStartField = null; }
|
||
} else {
|
||
save.field = null;
|
||
save.turnStartField = null;
|
||
}
|
||
// Отложенная партия (если игрок в меню): сохраняем снимок целиком, чтобы
|
||
// «Продолжить партию» пережило reload.
|
||
if (suspended) {
|
||
try {
|
||
save.suspended = {
|
||
state: suspended.state, turn: suspended.turn,
|
||
fieldW: suspended.fieldW, fieldH: suspended.fieldH, wrap: suspended.wrap,
|
||
field: bytesToB64(suspended.field),
|
||
turnStartField: suspended.turnStartField ? bytesToB64(suspended.turnStartField) : null,
|
||
stable: suspended.stable,
|
||
campaign: !!suspended.campaign,
|
||
levelIndex: (suspended.campaign && Number.isInteger(suspended.levelIndex)) ? suspended.levelIndex : null,
|
||
budget: {
|
||
total: suspended.budget.total, // лимит отложенной игры, а не текущий активный (D2)
|
||
used1: suspended.budget.used1, used2: suspended.budget.used2,
|
||
start1: suspended.budget.start1, start2: suspended.budget.start2
|
||
}
|
||
};
|
||
} catch (e) { save.suspended = null; }
|
||
} else {
|
||
save.suspended = null;
|
||
}
|
||
try { localStorage.setItem(SAVE_KEY, JSON.stringify(save)); } catch (e) {}
|
||
}
|
||
|
||
// Применить загруженный сейв при старте: настройки + (если есть) резюм партии.
|
||
function applySave(s) {
|
||
if (!s) {
|
||
save = freshSave(); suspended = null; confirmAction = null; confirmResume = false;
|
||
campaign = false; levelIndex = -1; zoneRect = null; pendingLevel = null;
|
||
campaignProgress = normalizeProgress(null);
|
||
menuFieldW = DEFAULT_FIELD; menuFieldH = DEFAULT_FIELD; menuWrap = false; menuBudget = DEFAULT_BUDGET;
|
||
applyTheme('dark');
|
||
applyMenuSettings(); // активная игра = пользовательским дефолтам (единый путь, D1)
|
||
return;
|
||
}
|
||
save = s;
|
||
renderDirty = true; // поле/маска/размер могли измениться — пересобрать offscreen
|
||
// Отложенная партия (кнопка «Продолжить партию» в меню).
|
||
suspended = s.suspended || null;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
pendingLevel = null;
|
||
// Кампания сбрасывается; в ветке резюма восстановится из сейва + конфига уровня.
|
||
campaign = false;
|
||
levelIndex = -1;
|
||
zoneRect = null;
|
||
campaignProgress = normalizeProgress(s.progress);
|
||
// Настройки. Активная игра — из fieldW/fieldH/wrap/budget (для резюма, в т.ч. уровня
|
||
// кампании); пользовательские настройки меню — из menu* (старые сейвы: = активным).
|
||
fieldW = isValidSize(s.fieldW) ? s.fieldW : DEFAULT_FIELD;
|
||
fieldH = isValidSize(s.fieldH) ? s.fieldH : fieldW;
|
||
wrap = !!s.wrap;
|
||
menuFieldW = isValidSize(s.menuFieldW) ? s.menuFieldW : fieldW;
|
||
menuFieldH = isValidSize(s.menuFieldH) ? s.menuFieldH : fieldH;
|
||
menuWrap = (s.menuWrap === undefined) ? wrap : !!s.menuWrap;
|
||
menuBudget = (typeof s.menuBudget === 'number' && Number.isFinite(s.menuBudget) && s.menuBudget >= 0)
|
||
? Math.floor(s.menuBudget) : normalizeBudget(s.budget).total;
|
||
applyTheme(s.theme);
|
||
STEPS_PER_SEC = (typeof s.stepsPerSec === 'number' && s.stepsPerSec > 0) ? s.stepsPerSec : 8;
|
||
window.STEPS_PER_SEC = STEPS_PER_SEC;
|
||
tool = findTool(s.tool) || TOOLS[0];
|
||
const b = normalizeBudget(s.budget);
|
||
budget.total = b.total;
|
||
budget.used1 = b.used1;
|
||
budget.used2 = b.used2;
|
||
turnStartBudget = { used1: b.start1, used2: b.start2 };
|
||
rebuildNeighborTables();
|
||
// Резюм незавершённой партии: поле ЦЕЛОЕ (скрытие — только визуальное).
|
||
if (isResumable(s.state)) {
|
||
let f = null;
|
||
try { f = s.field ? b64ToBytes(s.field) : null; } catch (e) { f = null; }
|
||
if (f && f.length === fieldW * fieldH) {
|
||
state = s.state;
|
||
mode = modeForState(state);
|
||
turn = s.turn === 2 ? 2 : 1;
|
||
reason = null;
|
||
winner = null;
|
||
field = f;
|
||
next = new Uint8Array(fieldW * fieldH);
|
||
let ts = null;
|
||
try { ts = s.turnStartField ? b64ToBytes(s.turnStartField) : null; } catch (e) { ts = null; }
|
||
turnStartField = (ts && ts.length === fieldW * fieldH) ? ts
|
||
: (isDrawState(state) ? field.slice() : null);
|
||
// Кампания: восстанавливаем режим/уровень/прямоугольник зоны из сейва и конфига.
|
||
if (s.campaign && Number.isInteger(s.levelIndex) && CAMPAIGN_LEVELS[s.levelIndex]) {
|
||
campaign = true;
|
||
levelIndex = s.levelIndex;
|
||
zoneRect = campaignZoneFor(levelIndex);
|
||
}
|
||
// Устойчивость: на паузе восстанавливаем сохранённый флаг; в RUNNING — заново
|
||
// засеваем окно (детекция с этого поколения, как при обычном старте).
|
||
if (state === 'RUNNING') seedStability();
|
||
else { recentHashes.length = 0; fieldStable = !!s.stable; }
|
||
simAcc = 0;
|
||
last = performance.now();
|
||
return;
|
||
}
|
||
}
|
||
// Нет незавершённой/валидной партии — чистый старт в меню с настройками ПОЛЬЗОВАТЕЛЯ.
|
||
// Активная игра могла остаться от уровня кампании (напр. 30×30 / бюджет 20) — тогда
|
||
// fieldW/budget.total тянули бы настройки уровня; applyMenuSettings() возвращает их к
|
||
// menu* (D1). Покрывает и фолбэк «резюмируемое состояние, но битое поле».
|
||
applyMenuSettings();
|
||
state = 'MENU';
|
||
mode = 'menu';
|
||
turn = 1;
|
||
reason = null;
|
||
winner = null;
|
||
}
|
||
|
||
// ==== ОТРИСОВКА ====
|
||
var hoverCell = null; // клетка под указателем (для ghost-превью)
|
||
var painting = false; // идёт ли рисование (drag)
|
||
var lastCell = null; // последняя обработанная клетка при drag
|
||
// Инструмент, временно подменяющий активный на время drag (правой кнопкой — ластик).
|
||
// Не трогает глобал tool (иначе поехали бы подсветка тулбара/сейв): paintTool смотрит сюда.
|
||
var paintOverride = null;
|
||
|
||
// Производительный слой клеток для больших полей (до 300×300 = 90 000 клеток):
|
||
// offscreen-канва fieldW×fieldH (1 px = 1 клетка) + ImageData. Пересобирается только
|
||
// при изменении поля/маски/размера (флаг renderDirty), затем масштабируется на доску
|
||
// одним drawImage. Цвет живых клеток остаётся точным (nearest-neighbor, alpha=255).
|
||
var fieldCanvas = document.createElement('canvas');
|
||
var fieldCtx = fieldCanvas.getContext('2d');
|
||
var fieldImage = null; // ImageData fieldW×fieldH
|
||
function hexToRgb(hex) {
|
||
const n = parseInt(hex.slice(1), 16);
|
||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||
}
|
||
const P1_RGB = hexToRgb(COLORS.p1);
|
||
const P2_RGB = hexToRgb(COLORS.p2);
|
||
|
||
// Пересборка offscreen-слоя: один проход по полю, RGBA цвета игрока для ВИДИМЫХ маской
|
||
// клеток (alpha 255), alpha 0 — для скрытых/мёртвых. Рендер не мутирует field.
|
||
function rebuildFieldImage() {
|
||
if (!fieldImage || fieldImage.width !== fieldW || fieldImage.height !== fieldH) {
|
||
fieldCanvas.width = fieldW;
|
||
fieldCanvas.height = fieldH;
|
||
fieldImage = fieldCtx.createImageData(fieldW, fieldH);
|
||
}
|
||
const mask = visibleMask();
|
||
const show1 = mask.indexOf(1) >= 0, show2 = mask.indexOf(2) >= 0;
|
||
const d = fieldImage.data;
|
||
const r1 = P1_RGB[0], g1 = P1_RGB[1], b1 = P1_RGB[2];
|
||
const r2 = P2_RGB[0], g2 = P2_RGB[1], b2 = P2_RGB[2];
|
||
for (let i = 0, j = 0; i < field.length; i++, j += 4) {
|
||
const v = field[i];
|
||
if (v === 1 && show1) { d[j] = r1; d[j + 1] = g1; d[j + 2] = b1; d[j + 3] = 255; }
|
||
else if (v === 2 && show2) { d[j] = r2; d[j + 1] = g2; d[j + 2] = b2; d[j + 3] = 255; }
|
||
else { d[j] = 0; d[j + 1] = 0; d[j + 2] = 0; d[j + 3] = 0; }
|
||
}
|
||
fieldCtx.putImageData(fieldImage, 0, 0);
|
||
renderDirty = false;
|
||
}
|
||
|
||
// Зона рисования кампании — прямоугольник zoneRect (под слоем клеток, чтобы не
|
||
// искажать их точный цвет). active (фазы рисования): затемняем ВСЁ вне прямоугольника
|
||
// и мягко тонируем его цветом игрока + рамка; иначе (RUNNING/PAUSED): лёгкий тинт +
|
||
// рамка-ориентир без затемнения.
|
||
function drawCampaignZone(active) {
|
||
if (!zoneRect) return;
|
||
const cs = board.cell, bx = board.x, by = board.y, bw = board.w, bh = board.h;
|
||
const rx = bx + zoneRect.x * cs, ry = by + zoneRect.y * cs;
|
||
const rw = zoneRect.w * cs, rh = zoneRect.h * cs;
|
||
const own = turn === 1 ? P1_RGB : P2_RGB;
|
||
if (active) {
|
||
// Затемнение четырёх областей вокруг прямоугольника (может покрывать всё поле — тогда пусто).
|
||
ctx.fillStyle = COLORS.zoneDim;
|
||
if (ry > by) ctx.fillRect(bx, by, bw, ry - by);
|
||
if (ry + rh < by + bh) ctx.fillRect(bx, ry + rh, bw, by + bh - (ry + rh));
|
||
if (rx > bx) ctx.fillRect(bx, ry, rx - bx, rh);
|
||
if (rx + rw < bx + bw) ctx.fillRect(rx + rw, ry, bx + bw - (rx + rw), rh);
|
||
ctx.fillStyle = 'rgba(' + own[0] + ',' + own[1] + ',' + own[2] + ',.10)';
|
||
ctx.fillRect(rx, ry, rw, rh);
|
||
} else {
|
||
ctx.globalAlpha = 0.05;
|
||
ctx.fillStyle = turn === 1 ? COLORS.p1 : COLORS.p2;
|
||
ctx.fillRect(rx, ry, rw, rh);
|
||
ctx.globalAlpha = 1;
|
||
}
|
||
ctx.strokeStyle = COLORS.zoneLine;
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeRect(Math.round(rx) + 0.5, Math.round(ry) + 0.5, rw, rh);
|
||
}
|
||
|
||
function draw() {
|
||
ctx.fillStyle = COLORS.bg;
|
||
ctx.fillRect(0, 0, viewW, viewH);
|
||
if (!field) return;
|
||
|
||
const cs = board.cell, bx = board.x, by = board.y, bw = board.w, bh = board.h;
|
||
|
||
// Подсветка зон рисования (только PLACE_*/EDIT_*): свою половину мягко тонируем
|
||
// цветом текущего игрока (10%), чужую затемняем — оба оверлея ПОД клетками
|
||
// (точный цвет живых клеток не искажается). Плюс тонкая линия границы.
|
||
if (isDrawState(state)) {
|
||
// Кампания: зона — прямоугольник (затемняем всё вне него). Иначе — половина.
|
||
if (campaign && zoneRect) drawCampaignZone(true);
|
||
else {
|
||
const split = zoneSplit().boundary;
|
||
// Тинт своей половины 10% — строим из RGB игрока (без дублирования hex).
|
||
const own = turn === 1 ? P1_RGB : P2_RGB;
|
||
ctx.fillStyle = 'rgba(' + own[0] + ',' + own[1] + ',' + own[2] + ',.10)';
|
||
if (turn === 1) ctx.fillRect(bx, by, split * cs, bh);
|
||
else ctx.fillRect(bx + split * cs, by, (fieldW - split) * cs, bh);
|
||
// Затемнение чужой половины.
|
||
ctx.fillStyle = COLORS.zoneDim;
|
||
if (turn === 1) ctx.fillRect(bx + split * cs, by, (fieldW - split) * cs, bh); // затемняем правую (чужая)
|
||
else ctx.fillRect(bx, by, split * cs, bh); // затемняем левую (чужая)
|
||
ctx.strokeStyle = COLORS.zoneLine;
|
||
ctx.lineWidth = 1;
|
||
const gx = Math.round(bx + split * cs) + 0.5;
|
||
ctx.beginPath(); ctx.moveTo(gx, by); ctx.lineTo(gx, by + bh); ctx.stroke();
|
||
}
|
||
} else if (state === 'RUNNING' || state === 'PAUSED') {
|
||
// При симуляции/паузе — мягкая (5%) подсветка территорий ПОД клетками. В кампании
|
||
// это ориентир зоны-прямоугольника; в обычной игре — половины без затемнения.
|
||
if (campaign && zoneRect) drawCampaignZone(false);
|
||
else {
|
||
const split = zoneSplit().boundary;
|
||
ctx.globalAlpha = 0.05;
|
||
ctx.fillStyle = COLORS.p1;
|
||
ctx.fillRect(bx, by, split * cs, bh);
|
||
ctx.fillStyle = COLORS.p2;
|
||
ctx.fillRect(bx + split * cs, by, (fieldW - split) * cs, bh);
|
||
ctx.globalAlpha = 1;
|
||
ctx.strokeStyle = COLORS.zoneLine;
|
||
ctx.lineWidth = 1;
|
||
const gx = Math.round(bx + split * cs) + 0.5;
|
||
ctx.beginPath(); ctx.moveTo(gx, by); ctx.lineTo(gx, by + bh); ctx.stroke();
|
||
}
|
||
}
|
||
|
||
// Клетки: offscreen-слой (пересобирается при renderDirty) одним масштабированным
|
||
// drawImage. imageSmoothingEnabled=false — nearest-neighbor, без размытия/подмеса.
|
||
if (renderDirty) rebuildFieldImage();
|
||
ctx.imageSmoothingEnabled = false;
|
||
ctx.drawImage(fieldCanvas, bx, by, bw, bh);
|
||
|
||
// Сетка — только когда клетка достаточно крупная.
|
||
if (cs >= 5) {
|
||
ctx.strokeStyle = COLORS.grid;
|
||
ctx.lineWidth = 1;
|
||
ctx.beginPath();
|
||
for (let i = 0; i <= fieldW; i++) {
|
||
const gx = Math.round(bx + i * cs) + 0.5;
|
||
ctx.moveTo(gx, by); ctx.lineTo(gx, by + bh);
|
||
}
|
||
for (let j = 0; j <= fieldH; j++) {
|
||
const gy = Math.round(by + j * cs) + 0.5;
|
||
ctx.moveTo(bx, gy); ctx.lineTo(bx + bw, gy);
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
|
||
// Ghost-превью инструмента под указателем (вне drag).
|
||
if (hoverCell && isDrawState(state)) {
|
||
const gTool = paintOverride || tool;
|
||
const ghostColor = turn === 1 ? 'rgba(231,76,60,.35)' : 'rgba(46,204,113,.35)';
|
||
if (stamp && !gTool.erase) {
|
||
// Превью штампа: якорь — левый верхний угол bbox. Клетки в своей зоне — цветом
|
||
// игрока, вылет за зону — тревожным цветом (игрок видит, что часть не поместится);
|
||
// клетки за пределами доски не рисуем.
|
||
const cells = stampCells(); // с учётом поворота колесом (stampRot)
|
||
for (let k = 0; k < cells.length; k++) {
|
||
const hx = hoverCell.x + cells[k].x, hy = hoverCell.y + cells[k].y;
|
||
if (hx < 0 || hy < 0 || hx >= fieldW || hy >= fieldH) continue;
|
||
ctx.fillStyle = inZoneAt(hx, hy, turn) ? ghostColor : COLORS.stampOut;
|
||
ctx.fillRect(bx + hx * cs, by + hy * cs, cs, cs);
|
||
}
|
||
} else {
|
||
const half = gTool.size >> 1;
|
||
ctx.fillStyle = gTool.erase ? COLORS.eraseGhost : ghostColor;
|
||
for (let dy = -half; dy < gTool.size - half; dy++) {
|
||
for (let dx = -half; dx < gTool.size - half; dx++) {
|
||
const hx = hoverCell.x + dx, hy = hoverCell.y + dy;
|
||
if (hx < 0 || hy < 0 || hx >= fieldW || hy >= fieldH) continue;
|
||
if (!inZoneAt(hx, hy, turn)) continue; // превью только в своей зоне рисования
|
||
ctx.fillRect(bx + hx * cs, by + hy * cs, cs, cs);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Рамка доски.
|
||
ctx.strokeStyle = COLORS.border;
|
||
ctx.lineWidth = 2;
|
||
ctx.strokeRect(bx - 1, by - 1, bw + 2, bh + 2);
|
||
}
|
||
|
||
// ==== ЦИКЛ ====
|
||
var last = performance.now();
|
||
var simAcc = 0;
|
||
var lastPersistAt = 0; // метка времени последней записи сейва (троттлинг RUNNING)
|
||
function tick() {
|
||
step(field, next);
|
||
const t = field; field = next; next = t; // двойной буфер
|
||
renderDirty = true; // поле изменилось
|
||
checkEnd();
|
||
// Детекция устойчивости: если новое поколение уже встречалось среди последних
|
||
// STABLE_WINDOW — поле вошло в фиксированную точку/короткий цикл. Тогда симуляция
|
||
// сама встаёт на паузу и открывает дорисовку (см. startEdit/fieldStable).
|
||
if (state === 'RUNNING') {
|
||
const h = hashField(field);
|
||
if (recentHashes.indexOf(h) >= 0) {
|
||
fieldStable = true;
|
||
state = 'PAUSED';
|
||
mode = modeForState(state);
|
||
persist(); // фиксируем стабильную паузу немедленно (не ждём троттлинг RUNNING)
|
||
checkStalemate(); // отстающему нечем ходить — сразу финал (см. checkStalemate)
|
||
// Кампания: игрок один и ему всегда есть что дорисовать, поэтому сразу открываем
|
||
// фазу дорисовки — не надо каждый раз жать «Дорисовать». Если авто-пауза завершила
|
||
// партию поражением (checkStalemate), состояние уже GAME_OVER — дорисовку не зовём.
|
||
if (campaign && state === 'PAUSED') startEdit(1);
|
||
} else {
|
||
recentHashes.push(h);
|
||
if (recentHashes.length > STABLE_WINDOW) recentHashes.shift();
|
||
}
|
||
}
|
||
// Сейв в RUNNING троттлим по времени (~раз в 3 с): запись больших полей недёшева.
|
||
const now = performance.now();
|
||
if (state === 'RUNNING' && now - lastPersistAt >= PERSIST_INTERVAL_MS) {
|
||
lastPersistAt = now;
|
||
persist();
|
||
}
|
||
}
|
||
function frame(now) {
|
||
const dt = Math.min(0.05, (now - last) / 1000); // кламп dt — переживаем просадки fps
|
||
last = now;
|
||
if (mode === 'run') {
|
||
simAcc += dt;
|
||
const stepDt = 1 / (STEPS_PER_SEC * speedMult);
|
||
let guard = 0;
|
||
while (simAcc >= stepDt && guard < 8) {
|
||
tick();
|
||
simAcc -= stepDt;
|
||
guard++;
|
||
if (state !== 'RUNNING') break;
|
||
}
|
||
} else {
|
||
simAcc = 0;
|
||
}
|
||
draw();
|
||
syncDom();
|
||
requestAnimationFrame(frame);
|
||
}
|
||
|
||
// ==== ВВОД (только Pointer Events) ====
|
||
// Координаты указателя в CSS-пикселях относительно холста.
|
||
function evtPos(e) {
|
||
const r = canvas.getBoundingClientRect();
|
||
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
||
}
|
||
// Экранные координаты → клетка (или null, если вне доски).
|
||
function pixelToCell(px, py) {
|
||
const cs = board.cell;
|
||
const x = Math.floor((px - board.x) / cs);
|
||
const y = Math.floor((py - board.y) / cs);
|
||
if (x < 0 || y < 0 || x >= fieldW || y >= fieldH) return null;
|
||
return { x: x, y: y };
|
||
}
|
||
// Применить инструмент в точке (CSS-пиксели холста). Публичный тест-форс.
|
||
// Вне поля или вне своей зоны рисования — не действует (false).
|
||
function applyIntent(px, py) {
|
||
if (!isDrawState(state)) return false;
|
||
const c = pixelToCell(px, py);
|
||
if (!c) return false;
|
||
if (!inZoneAt(c.x, c.y, turn)) return false;
|
||
// Возврат по факту изменения: клик по клетке врага (в кампании) или по своей клетке
|
||
// ничего не меняет → false (иначе «действие применилось» ложно, D5).
|
||
return paintTool(c.x, c.y) > 0;
|
||
}
|
||
// Интерполяция линии между клетками (Брезенхэм) — без разрывов при drag.
|
||
// Внутри одной половины x монотонен, поэтому линия не выходит из зоны.
|
||
function paintLine(a, b) {
|
||
let x0 = a.x, y0 = a.y;
|
||
const x1 = b.x, y1 = b.y;
|
||
const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
|
||
const sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
|
||
let err = dx + dy;
|
||
for (;;) {
|
||
paintTool(x0, y0);
|
||
if (x0 === x1 && y0 === y1) break;
|
||
const e2 = 2 * err;
|
||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||
if (e2 <= dx) { err += dx; y0 += sy; }
|
||
}
|
||
}
|
||
// Инструмент для кнопки мыши: правая (button 2) — ластик, остальные — активный.
|
||
// Если выбран ластик, ПКМ сохраняет его размер (1×1/4×4), иначе берём базовый 1×1.
|
||
function toolForButton(button) {
|
||
if (button === 2) return (tool && tool.erase) ? tool : (findTool('erase1') || TOOLS[2]);
|
||
return tool;
|
||
}
|
||
function onPointerDown(e) {
|
||
e.preventDefault();
|
||
if (e.button !== 0 && e.button !== 2) return; // только ЛКМ/ПКМ (тач/перо — button 0)
|
||
const p = evtPos(e);
|
||
const c = pixelToCell(p.x, p.y);
|
||
if (!isDrawState(state) || !c) return;
|
||
hoverCell = c;
|
||
if (!inZoneAt(c.x, c.y, turn)) return; // вне своей зоны — рисование не начинаем
|
||
const override = toolForButton(e.button);
|
||
// Режим «постановка по отпусканию»: нажатие только показывает превью, а ставит/стирает
|
||
// инструмент по pointerup (см. onPointerUp). Так палец не закрывает точку постановки.
|
||
// paintOverride храним на время удержания — превью сразу отражает ПКМ-ластик.
|
||
if (tapPlace) {
|
||
paintOverride = override;
|
||
pressing = true;
|
||
if (canvas.setPointerCapture) { try { canvas.setPointerCapture(e.pointerId); } catch (_) {} }
|
||
return;
|
||
}
|
||
// Штамп паттерна: одно нажатие = одна постановка (без drag-повтора — иначе зальёт
|
||
// бюджет повторами на каждом шаге линии). Якорь штампа может быть в своей зоне, а
|
||
// часть паттерна выйти за неё — там просто не ставится. Правая кнопка (ластик)
|
||
// паттерн не штампует: работает обычным ластиком.
|
||
if (stamp && !(override && override.erase)) {
|
||
paintOverride = null;
|
||
paintTool(c.x, c.y);
|
||
return;
|
||
}
|
||
paintOverride = override;
|
||
painting = true;
|
||
lastCell = c;
|
||
paintTool(c.x, c.y);
|
||
if (canvas.setPointerCapture) { try { canvas.setPointerCapture(e.pointerId); } catch (_) {} }
|
||
}
|
||
function onPointerMove(e) {
|
||
const p = evtPos(e);
|
||
const c = pixelToCell(p.x, p.y);
|
||
hoverCell = c;
|
||
if (tapPlace && pressing) return; // тап: превью следует за пальцем, постановка — на отпускании
|
||
if (painting) {
|
||
if (!c || !inZoneAt(c.x, c.y, turn)) {
|
||
lastCell = null; // вышли из зоны/доски — не продлеваем линию
|
||
} else {
|
||
if (lastCell && inZoneAt(lastCell.x, lastCell.y, turn)) paintLine(lastCell, c);
|
||
else paintTool(c.x, c.y);
|
||
lastCell = c;
|
||
}
|
||
}
|
||
}
|
||
function onPointerUp(e) {
|
||
// Тап: постановка происходит в момент отпускания (pointercancel — отмена, без действия).
|
||
if (tapPlace && pressing) {
|
||
if (!e || e.type !== 'pointercancel') {
|
||
const p = evtPos(e);
|
||
const c = pixelToCell(p.x, p.y);
|
||
if (c && inZoneAt(c.x, c.y, turn)) paintTool(c.x, c.y);
|
||
}
|
||
pressing = false;
|
||
}
|
||
painting = false;
|
||
lastCell = null;
|
||
paintOverride = null; // правый ластик действует только на время удержания
|
||
// Ghost-превью: после отпускания подсветка остаётся только если указатель на доске.
|
||
if (e) { const p = evtPos(e); hoverCell = pixelToCell(p.x, p.y); }
|
||
else hoverCell = null;
|
||
}
|
||
|
||
// ==== UI ====
|
||
const el = {};
|
||
['turnLabel', 'scoreP1', 'scoreP2', 'scores', 'menu', 'handoff', 'handoffText', 'handoffNote',
|
||
'gameover', 'gameoverText', 'gameoverSub', 'toolbar', 'actions',
|
||
'btnReady', 'btnReset', 'btnRestart', 'btnPause', 'btnEnd', 'btnEndPause', 'btnResume', 'speedRow',
|
||
'btnEditP1', 'btnEditP2', 'btnHandoff', 'btnHandoffBack', 'btnPlay', 'btnRevenge', 'btnPauseMenu', 'btnGameoverMenu',
|
||
'btnContinue', 'confirm', 'confirmHead', 'confirmText', 'btnConfirmOk', 'btnConfirmCancel',
|
||
'optSize', 'toolPencil', 'toolBrush', 'toolErase1', 'toolErase4',
|
||
'budgetLabel', 'optWrap', 'optBudget', 'optTheme', 'optPattern',
|
||
'patternMenu',
|
||
'btnRotate', 'rotDeg', 'btnTapPlace',
|
||
'setup', 'btnStartTwo', 'btnTwoBack',
|
||
'levels', 'levelList', 'btnCampaign', 'btnLevelsBack', 'btnNextLevel', 'btnGameoverLevels'].forEach(function (id) {
|
||
el[id] = document.getElementById(id);
|
||
});
|
||
const TOOL_BTN = { pencil: el.toolPencil, brush: el.toolBrush, erase1: el.toolErase1, erase4: el.toolErase4 };
|
||
// Кнопки ряда скорости (1×/2×/4×/8×) — общий обработчик на контейнере #speedRow.
|
||
const SPEED_BTNS = Array.prototype.slice.call(el.speedRow.querySelectorAll('.speedbtn'));
|
||
|
||
// Опции дропдауна размера поля строим из FIELD_DIMS (квадраты + прямоугольники),
|
||
// value = "WxH" (парсится в обработчике change), подпись — «W×H».
|
||
function buildSizeOptions() {
|
||
if (!el.optSize) return;
|
||
el.optSize.innerHTML = '';
|
||
FIELD_DIMS.forEach(function (d) {
|
||
const o = document.createElement('option');
|
||
o.value = d[0] + 'x' + d[1];
|
||
o.textContent = d[0] + '×' + d[1];
|
||
el.optSize.appendChild(o);
|
||
});
|
||
}
|
||
buildSizeOptions();
|
||
|
||
// Опции дропдауна паттернов строим из PATTERNS; первая (value="") — «нет паттерна».
|
||
// Остальные сгруппированы в optgroup по PATTERN_GROUPS (порядок групп и записей).
|
||
// Выбор хранится в глобале stamp (транзиентно): value="" → null.
|
||
function buildPatternOptions() {
|
||
if (!el.optPattern) return;
|
||
el.optPattern.innerHTML = '';
|
||
const none = document.createElement('option');
|
||
none.value = '';
|
||
none.textContent = 'Паттерн: нет';
|
||
el.optPattern.appendChild(none);
|
||
PATTERN_GROUPS.forEach(function (g) {
|
||
const og = document.createElement('optgroup');
|
||
og.label = g.label;
|
||
PATTERNS.forEach(function (p) {
|
||
if (p.group !== g.key) return;
|
||
const o = document.createElement('option');
|
||
o.value = p.id;
|
||
o.textContent = p.name;
|
||
og.appendChild(o);
|
||
});
|
||
if (og.children.length) el.optPattern.appendChild(og);
|
||
});
|
||
}
|
||
buildPatternOptions();
|
||
|
||
// Свой список паттернов (замена нативного popup). Нативный popup <select> на длинном
|
||
// списке (22 фигуры) в Chromium позиционируется поверх курсора, и обычный клик выбирает
|
||
// случайную фигуру — дропдаун «открывается на нажатии и закрывается на отпускании».
|
||
// Поэтому нативный popup подавляем (mousedown → preventDefault) и показываем свой
|
||
// список #patternMenu. Сам <select> остаётся контролом/контрактом: value/change/
|
||
// disabled/active/optgroup и page.select в тестах работают как раньше.
|
||
function buildPatternMenu() {
|
||
if (!el.patternMenu) return;
|
||
el.patternMenu.innerHTML = '';
|
||
const none = document.createElement('button');
|
||
none.type = 'button';
|
||
none.className = 'patitem';
|
||
none.setAttribute('data-id', '');
|
||
none.textContent = 'Паттерн: нет';
|
||
el.patternMenu.appendChild(none);
|
||
PATTERN_GROUPS.forEach(function (g) {
|
||
const head = document.createElement('div');
|
||
head.className = 'patgroup';
|
||
head.textContent = g.label;
|
||
el.patternMenu.appendChild(head);
|
||
PATTERNS.forEach(function (p) {
|
||
if (p.group !== g.key) return;
|
||
const b = document.createElement('button');
|
||
b.type = 'button';
|
||
b.className = 'patitem';
|
||
b.setAttribute('data-id', p.id);
|
||
b.textContent = p.name;
|
||
el.patternMenu.appendChild(b);
|
||
});
|
||
});
|
||
}
|
||
buildPatternMenu();
|
||
|
||
// Открыт ли свой список паттернов.
|
||
function patternMenuOpen() {
|
||
return !!(el.patternMenu && !el.patternMenu.classList.contains('hidden'));
|
||
}
|
||
// Подсветить в списке активный паттерн (stamp).
|
||
function syncPatternMenu() {
|
||
if (!el.patternMenu) return;
|
||
const cur = stamp ? stamp.id : '';
|
||
Array.prototype.forEach.call(el.patternMenu.querySelectorAll('.patitem'), function (b) {
|
||
b.classList.toggle('active', b.getAttribute('data-id') === cur);
|
||
});
|
||
}
|
||
// Позиция списка рядом с #optPattern: снизу, а если не влезает — сверху. Высота
|
||
// подрезается так, чтобы список НИКОГДА не перекрывал сам селект (иначе mouseup по
|
||
// селекту попал бы на фигуру — та же проблема, что у нативного popup).
|
||
function positionPatternMenu() {
|
||
const m = el.patternMenu, s = el.optPattern;
|
||
if (!m || !s) return;
|
||
const r = s.getBoundingClientRect();
|
||
const gap = 6, pad = 8, cap = 420;
|
||
const w = Math.max(180, Math.min(Math.max(r.width, 200), 320));
|
||
m.style.width = w + 'px';
|
||
m.style.maxHeight = ''; // сброс, чтобы измерить естественную высоту
|
||
const want = Math.min(m.scrollHeight || cap, cap);
|
||
const below = viewH - pad - (r.bottom + gap);
|
||
const above = (r.top - gap) - pad;
|
||
let top, avail;
|
||
if (below >= want) { top = r.bottom + gap; avail = below; }
|
||
else if (above >= want) { top = r.top - gap - want; avail = above; }
|
||
else if (below >= above) { top = r.bottom + gap; avail = below; }
|
||
else { avail = above; top = r.top - gap - avail; }
|
||
m.style.maxHeight = Math.max(120, Math.min(avail, cap)) + 'px';
|
||
let left = r.left;
|
||
if (left + w > viewW - pad) left = Math.max(pad, viewW - pad - w);
|
||
m.style.left = Math.round(left) + 'px';
|
||
m.style.top = Math.round(top) + 'px';
|
||
}
|
||
function closePatternMenu() {
|
||
if (el.patternMenu) el.patternMenu.classList.add('hidden');
|
||
}
|
||
function togglePatternMenu() {
|
||
if (!el.patternMenu || !el.optPattern || el.optPattern.disabled) return;
|
||
if (patternMenuOpen()) { closePatternMenu(); return; }
|
||
syncPatternMenu();
|
||
el.patternMenu.classList.remove('hidden');
|
||
positionPatternMenu();
|
||
}
|
||
|
||
// Список уровней кампании: кнопки по CAMPAIGN_LEVELS. Заблокированные (индекс >=
|
||
// campaignProgress.unlocked) — disabled с замком. Клик → requestCampaignLevel(i).
|
||
function buildLevelList() {
|
||
if (!el.levelList) return;
|
||
el.levelList.innerHTML = '';
|
||
CAMPAIGN_LEVELS.forEach(function (lv, i) {
|
||
const b = document.createElement('button');
|
||
b.className = 'act wide levelbtn';
|
||
b.dataset.level = String(i);
|
||
b.addEventListener('click', function () { requestCampaignLevel(i); });
|
||
el.levelList.appendChild(b);
|
||
});
|
||
syncLevelList();
|
||
}
|
||
// Обновить подписи/доступность кнопок уровней (вызывается из syncDom).
|
||
function syncLevelList() {
|
||
if (!el.levelList) return;
|
||
if (state !== 'LEVEL_SELECT') return; // 100 кнопок синхронятся только на экране уровней
|
||
const btns = el.levelList.children;
|
||
for (let i = 0; i < btns.length; i++) {
|
||
const idx = Number(btns[i].dataset.level);
|
||
if (!Number.isInteger(idx)) continue;
|
||
const locked = idx >= campaignProgress.unlocked;
|
||
btns[i].disabled = locked;
|
||
btns[i].textContent = (locked ? '🔒 ' : '') + 'Уровень ' + (idx + 1) + ' · ' + CAMPAIGN_LEVELS[idx].name;
|
||
}
|
||
}
|
||
buildLevelList();
|
||
|
||
function labelFor(s) {
|
||
switch (s) {
|
||
case 'MENU': return 'Меню';
|
||
case 'LEVEL_SELECT': return 'Кампания';
|
||
case 'PLACE_P1': return 'Игрок 1 · расстановка';
|
||
case 'PLACE_P2': return 'Игрок 2 · расстановка';
|
||
case 'EDIT_P1': return 'Игрок 1 · дорисовка';
|
||
case 'EDIT_P2': return 'Игрок 2 · дорисовка';
|
||
case 'HANDOFF_P1_P2': return 'Передача устройства';
|
||
case 'HANDOFF_P2_START': return 'Расстановка завершена';
|
||
case 'RUNNING': return 'Симуляция';
|
||
case 'PAUSED': return 'Пауза';
|
||
case 'GAME_OVER': return 'Итог партии';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
// Синхронизация HUD/кнопок/оверлеев с состоянием (каждый кадр).
|
||
function syncDom() {
|
||
const drawPhase = isDrawState(state);
|
||
const handoff = isHandoffState(state);
|
||
|
||
// Акцент текущего игрока (цвет) — только в фазах рисования: им подсвечиваются
|
||
// метка фазы и рамка боковой панели (CSS через --accent, см. стили).
|
||
document.body.classList.toggle('turn-p1', drawPhase && turn === 1);
|
||
document.body.classList.toggle('turn-p2', drawPhase && turn === 2);
|
||
|
||
// Метка фазы. В кампании — «Уровень N/T» (+ признак стабильности поля на паузе).
|
||
let phaseLabel;
|
||
if (campaign && levelIndex >= 0) {
|
||
phaseLabel = 'Уровень ' + (levelIndex + 1) + '/' + CAMPAIGN_LEVELS.length;
|
||
} else {
|
||
phaseLabel = labelFor(state);
|
||
}
|
||
el.turnLabel.textContent = phaseLabel
|
||
+ (state === 'PAUSED' && fieldStable ? ' · поле стабильно' : '');
|
||
|
||
// Счёт: в HANDOFF скрываем («—»). В кампании счёт не нужен вовсе — скрыт.
|
||
const s = countByPlayer(field);
|
||
el.scoreP1.textContent = handoff ? '—' : s.p1;
|
||
el.scoreP2.textContent = handoff ? '—' : s.p2;
|
||
if (el.scores) el.scores.classList.toggle('hidden', campaign);
|
||
|
||
// Бюджет текущего игрока: виден в фазах рисования, а в кампании — ещё и в
|
||
// RUNNING/PAUSED (HUD уровня показывает остаток на протяжении всего уровня).
|
||
// В HANDOFF/MENU/финале скрыт. При нуле — визуальное предупреждение (красный).
|
||
if (el.budgetLabel) {
|
||
const showBudget = drawPhase || (campaign && (state === 'RUNNING' || state === 'PAUSED'));
|
||
if (showBudget) {
|
||
const bp = campaign ? CAMPAIGN_PLAYER : turn; // в кампании бюджет всегда у игрока 1
|
||
const rem = remaining(bp);
|
||
// Компактный текст (N / total) — плашка не переполняет HUD на узких экранах (M1).
|
||
el.budgetLabel.textContent = rem + ' / ' + budget.total;
|
||
el.budgetLabel.classList.remove('hidden');
|
||
el.budgetLabel.classList.toggle('p1', bp === 1);
|
||
el.budgetLabel.classList.toggle('p2', bp === 2);
|
||
el.budgetLabel.classList.toggle('zero', rem <= 0);
|
||
} else {
|
||
el.budgetLabel.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// Настройки меню: размер поля, чекбокс «Замкнутая карта» и поле бюджета
|
||
// (не перебиваем ввод, пока контрол в фокусе).
|
||
// Меню-контролы отражают ПОЛЬЗОВАТЕЛЬСКИЕ настройки (menu*), а не активную игру:
|
||
// в кампании активный размер/wrap/бюджет — уровня, но меню должно показывать своё (D1).
|
||
if (el.optSize && document.activeElement !== el.optSize) el.optSize.value = menuFieldW + 'x' + menuFieldH;
|
||
if (el.optWrap) el.optWrap.checked = menuWrap;
|
||
if (el.optTheme) el.optTheme.value = theme;
|
||
if (el.optBudget && document.activeElement !== el.optBudget) el.optBudget.value = menuBudget;
|
||
|
||
// Оверлеи. Пауза — БЕЗ оверлея: её кнопки инлайн в нижней панели/боковой зоне.
|
||
el.menu.classList.toggle('hidden', state !== 'MENU');
|
||
el.handoff.classList.toggle('hidden', !handoff);
|
||
// Передаём вслепую игроку 2 — панель подсвечена его цветом.
|
||
el.handoff.classList.toggle('to-p2', state === 'HANDOFF_P1_P2');
|
||
el.gameover.classList.toggle('hidden', state !== 'GAME_OVER');
|
||
// Финал: рамка панели цветом победителя.
|
||
el.gameover.classList.toggle('win-p1', state === 'GAME_OVER' && winner === '1');
|
||
el.gameover.classList.toggle('win-p2', state === 'GAME_OVER' && winner === '2');
|
||
// Экран выбора уровня кампании.
|
||
if (el.levels) el.levels.classList.toggle('hidden', state !== 'LEVEL_SELECT');
|
||
// Экран «Игра на двоих»: своя справка и настройки поля (только обычная партия).
|
||
if (el.setup) el.setup.classList.toggle('hidden', state !== 'TWO_SETUP');
|
||
if (el.levelList) syncLevelList();
|
||
// Диалог подтверждения: текст и кнопка зависят от действия.
|
||
el.confirm.classList.toggle('hidden', !confirmAction);
|
||
if (confirmAction === 'campaign') {
|
||
el.confirmHead.textContent = 'Начать уровень?';
|
||
el.confirmText.textContent = 'Текущая партия будет потеряна.';
|
||
el.btnConfirmOk.textContent = 'Начать';
|
||
} else if (confirmAction === 'restart') {
|
||
el.confirmHead.textContent = 'Начать уровень заново?';
|
||
el.confirmText.textContent = 'Расстановка и бюджет уровня сбросятся.';
|
||
el.btnConfirmOk.textContent = 'Заново';
|
||
} else if (confirmAction === 'end') {
|
||
el.confirmHead.textContent = 'Завершить партию?';
|
||
el.confirmText.textContent = 'Партия завершится досрочно — победитель определится по текущим клеткам.';
|
||
el.btnConfirmOk.textContent = 'Завершить';
|
||
} else {
|
||
el.confirmHead.textContent = 'Начать новую партию?';
|
||
el.confirmText.textContent = 'Незавершённая партия будет потеряна.';
|
||
el.btnConfirmOk.textContent = 'Начать новую';
|
||
}
|
||
if (handoff) {
|
||
const startGate = state === 'HANDOFF_P2_START';
|
||
el.handoffText.textContent = startGate ? 'Расстановка завершена' : 'Передайте устройство игроку 2';
|
||
el.handoffNote.classList.toggle('hidden', startGate);
|
||
el.btnHandoff.textContent = startGate ? 'Начать симуляцию' : 'Передать';
|
||
el.btnHandoffBack.classList.toggle('hidden', !startGate); // возврат к P1 — после расстановки P2
|
||
}
|
||
// «Продолжить партию» — только в меню и только если есть отложенная партия.
|
||
if (el.btnContinue) el.btnContinue.classList.toggle('hidden', !(state === 'MENU' && suspended));
|
||
|
||
// Нижние панели. Действия видны в рисовании, симуляции и на паузе (пауза инлайн).
|
||
const paused = state === 'PAUSED';
|
||
const canEdit = paused && fieldStable; // дорисовка — только когда поле устойчиво
|
||
el.toolbar.classList.toggle('hidden', !drawPhase);
|
||
el.actions.classList.toggle('hidden', !(drawPhase || state === 'RUNNING' || paused));
|
||
el.btnReady.classList.toggle('hidden', !drawPhase);
|
||
el.btnReset.classList.toggle('hidden', !drawPhase);
|
||
// «Заново» (перезапуск уровня) — только в кампании и только когда идёт уровень.
|
||
if (el.btnRestart) el.btnRestart.classList.toggle('hidden', !(campaign && levelIndex >= 0));
|
||
el.btnPause.classList.toggle('hidden', state !== 'RUNNING');
|
||
// «Завершить партию» в кампании не нужна (исход определяют победа/поражение).
|
||
el.btnEnd.classList.toggle('hidden', state !== 'RUNNING' || campaign);
|
||
// Скорость: ряд кнопок 1×/2×/4×/8×, активна текущая; виден только во время симуляции.
|
||
el.speedRow.classList.toggle('hidden', state !== 'RUNNING');
|
||
SPEED_BTNS.forEach(function (b) { b.classList.toggle('active', Number(b.dataset.speed) === speedMult); });
|
||
// На паузе — отдельные кнопки дорисовки каждого игрока и запуск симуляции.
|
||
// Дорисовка доступна ТОЛЬКО когда поле стабильно (canEdit).
|
||
el.btnResume.classList.toggle('hidden', !paused);
|
||
el.btnEditP1.classList.toggle('hidden', !canEdit);
|
||
el.btnEditP2.classList.toggle('hidden', !canEdit || campaign); // в кампании игрок один
|
||
el.btnEndPause.classList.toggle('hidden', !paused || campaign);
|
||
el.btnPauseMenu.classList.toggle('hidden', !paused);
|
||
// Финал кампании: «Следующий уровень» (только при победе и наличии следующего)
|
||
// и «К уровням»; «Реванш» превращается в «Заново».
|
||
if (el.btnNextLevel) el.btnNextLevel.classList.toggle('hidden',
|
||
!(state === 'GAME_OVER' && campaign && reason === 'win' && levelIndex + 1 < CAMPAIGN_LEVELS.length));
|
||
if (el.btnGameoverLevels) el.btnGameoverLevels.classList.toggle('hidden',
|
||
!(state === 'GAME_OVER' && campaign));
|
||
if (el.btnRevenge) el.btnRevenge.textContent = (campaign && state === 'GAME_OVER') ? 'Заново' : 'Реванш';
|
||
|
||
// Инструменты: активная подсветка + доступность только в фазах рисования.
|
||
// Пока выбран паттерн (stamp) — ни одна кнопка инструмента не подсвечена: штамп
|
||
// перекрывает активный инструмент (кроме ПКМ-ластика, который действует всегда).
|
||
TOOLS.forEach(function (t) {
|
||
const b = TOOL_BTN[t.id];
|
||
if (!b) return;
|
||
b.classList.toggle('active', tool === t && !stamp);
|
||
b.disabled = !drawPhase;
|
||
});
|
||
// Дропдаун паттернов: доступен только в фазах рисования; значение не перебиваем,
|
||
// пока контрол в фокусе (как у размера поля). Активен (подсвечен) при выбранном штампе.
|
||
if (el.optPattern) {
|
||
if (document.activeElement !== el.optPattern) el.optPattern.value = stamp ? stamp.id : '';
|
||
el.optPattern.disabled = !drawPhase;
|
||
el.optPattern.classList.toggle('active', !!stamp);
|
||
// Свой список: вне фаз рисования закрываем, иначе держим подсветку активной фигуры.
|
||
if (patternMenuOpen()) {
|
||
if (!drawPhase) closePatternMenu();
|
||
else syncPatternMenu();
|
||
}
|
||
}
|
||
// Кнопка поворота паттерна: видна только в фазах рисования и при выбранном штампе.
|
||
// Подпись показывает текущий угол — полезно, когда палец закрывает превью.
|
||
if (el.btnRotate) {
|
||
el.btnRotate.classList.toggle('hidden', !(drawPhase && stamp));
|
||
if (el.rotDeg) el.rotDeg.textContent = (stampRot * 90) + '°';
|
||
}
|
||
// Режим «постановка по отпусканию»: тумблер (подсвечен, когда включён).
|
||
if (el.btnTapPlace) {
|
||
el.btnTapPlace.classList.toggle('active', tapPlace);
|
||
el.btnTapPlace.classList.toggle('hidden', !drawPhase);
|
||
}
|
||
|
||
// Итог партии.
|
||
if (state === 'GAME_OVER') {
|
||
if (campaign) {
|
||
if (reason === 'win') {
|
||
el.gameoverText.textContent = 'Победа!';
|
||
el.gameoverSub.textContent = 'Уровень ' + (levelIndex + 1) + ' пройден';
|
||
} else if (reason === 'manual') {
|
||
// Ручное завершение уровня (endGame/requestEndGame): это не поражение (D3).
|
||
el.gameoverText.textContent = 'Уровень прерван';
|
||
el.gameoverSub.textContent = 'Уровень ' + (levelIndex + 1) + ' не завершён';
|
||
} else {
|
||
el.gameoverText.textContent = 'Поражение';
|
||
el.gameoverSub.textContent = 'Осталось клеток врага: ' + s.p2 + ' · бюджет исчерпан';
|
||
}
|
||
} else {
|
||
const pre = winner === '1' ? 'Игрок 1 победил ' : winner === '2' ? 'Игрок 2 победил ' : 'Ничья ';
|
||
el.gameoverText.textContent = pre + s.p1 + ':' + s.p2;
|
||
el.gameoverSub.textContent = reason === 'manual'
|
||
? 'Партия завершена вручную'
|
||
: reason === 'stalemate'
|
||
? (winner === 'draw' ? 'У обоих игроков закончился бюджет' : 'У отстающего игрока закончился бюджет')
|
||
: 'У одного из игроков не осталось клеток';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Переключение активного инструмента по id.
|
||
function selectTool(id) {
|
||
const t = findTool(id);
|
||
if (t) tool = t;
|
||
stamp = null; // выбор инструмента сбрасывает активный паттерн («последнее действие побеждает»)
|
||
stampRot = 0; // вместе с паттерном сбрасываем и его поворот
|
||
wheelAccum = 0; // и накопленную дельту колеса
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Смена размера поля на произвольный W×H (квадратный или прямоугольный) — сброс доски.
|
||
function setFieldDim(w, h) {
|
||
if (!isValidSize(w) || !isValidSize(h)) return;
|
||
if (campaign) return; // в кампании доска уровня: смена размера потеряла бы врага (D4)
|
||
fieldW = w; fieldH = h;
|
||
// Пользовательская настройка (из меню) — переживёт кампанию и reload.
|
||
menuFieldW = w; menuFieldH = h;
|
||
field = new Uint8Array(w * h);
|
||
next = new Uint8Array(w * h);
|
||
turnStartField = null;
|
||
rebuildNeighborTables();
|
||
resetStability(); // новое поле — устойчивость не оценивалась
|
||
renderDirty = true; // сменился размер поля/offscreen
|
||
computeBoard();
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// Смена квадратного размера поля из меню (значение — только из FIELD_SIZES).
|
||
function setFieldSize(n) {
|
||
if (FIELD_SIZES.indexOf(n) < 0) return; // чужие значения игнорируем (квадрат только из FIELD_SIZES)
|
||
setFieldDim(n, n);
|
||
}
|
||
|
||
// Отложить текущую (резюмируемую) партию в меню, чтобы «Продолжить партию»
|
||
// вернуло ровно в неё. Снимок хранит размер/wrap/поле/бюджет: настройки в меню
|
||
// могли измениться, а прерванная партия обязана восстановиться целиком.
|
||
function suspendGame() {
|
||
if (!isResumable(state) || !field || field.length !== fieldW * fieldH) return;
|
||
suspended = {
|
||
state: state,
|
||
turn: turn,
|
||
fieldW: fieldW,
|
||
fieldH: fieldH,
|
||
wrap: wrap,
|
||
field: field.slice(),
|
||
turnStartField: turnStartField ? turnStartField.slice() : null,
|
||
stable: fieldStable,
|
||
budget: {
|
||
total: budget.total, // лимит тоже храним: отложенная игра возвращается с ним же (D2)
|
||
used1: budget.used1, used2: budget.used2,
|
||
start1: turnStartBudget.used1, start2: turnStartBudget.used2
|
||
},
|
||
campaign: campaign,
|
||
levelIndex: (campaign && levelIndex >= 0) ? levelIndex : null
|
||
};
|
||
}
|
||
|
||
// Вернуться из меню к отложенной партии (восстанавливает размер/wrap/поле/бюджет).
|
||
function resumeFromMenu() {
|
||
if (!suspended) return;
|
||
const s = suspended;
|
||
suspended = null;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
fieldW = s.fieldW;
|
||
fieldH = s.fieldH;
|
||
wrap = s.wrap;
|
||
rebuildNeighborTables();
|
||
field = s.field;
|
||
next = new Uint8Array(fieldW * fieldH);
|
||
turnStartField = s.turnStartField;
|
||
// Кампания: восстановить режим/уровень/зону-прямоугольник (если откладывали уровень).
|
||
campaign = !!(s.campaign && Number.isInteger(s.levelIndex) && CAMPAIGN_LEVELS[s.levelIndex]);
|
||
levelIndex = campaign ? s.levelIndex : -1;
|
||
zoneRect = campaign ? campaignZoneFor(levelIndex) : null;
|
||
// Лимит бюджета — из снимка отложенной игры (смена бюджета в меню её не трогает, D2).
|
||
budget.total = (typeof s.budget.total === 'number' && s.budget.total >= 0) ? s.budget.total : menuBudget;
|
||
budget.used1 = s.budget.used1;
|
||
budget.used2 = s.budget.used2;
|
||
turnStartBudget = { used1: s.budget.start1, used2: s.budget.start2 };
|
||
state = s.state;
|
||
turn = s.turn;
|
||
mode = modeForState(state);
|
||
if (state === 'RUNNING') seedStability();
|
||
else { recentHashes.length = 0; fieldStable = !!s.stable; }
|
||
renderDirty = true;
|
||
simAcc = 0;
|
||
last = performance.now();
|
||
lastPersistAt = performance.now();
|
||
computeBoard();
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// «Заново» в кампании: перезапуск уровня — с подтверждением (нельзя терять прогресс
|
||
// уровня одним кликом). На время диалога симуляция ставится на паузу; «Отмена» её возвращает.
|
||
function requestRestartLevel() {
|
||
if (!campaign || levelIndex < 0) return;
|
||
confirmResume = (state === 'RUNNING');
|
||
if (state === 'RUNNING') pauseGame();
|
||
confirmAction = 'restart';
|
||
syncDom();
|
||
}
|
||
// «Играть»/«Реванш»: если есть отложенная партия — сначала спросить подтверждение.
|
||
function requestNewGame() {
|
||
// Кампания: «Реванш»/«Заново» просто перезапускает текущий уровень.
|
||
if (campaign && levelIndex >= 0) { startLevel(levelIndex); return; }
|
||
if (suspended) { confirmAction = 'new'; confirmResume = false; syncDom(); return; }
|
||
newGame();
|
||
}
|
||
// Нажатие кнопки в диалоге подтверждения — выполняем отложенное действие.
|
||
function confirmProceed() {
|
||
const act = confirmAction;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
if (act === 'new') { suspended = null; newGame(); }
|
||
else if (act === 'end') { finish('manual'); }
|
||
else if (act === 'restart') {
|
||
if (campaign && levelIndex >= 0) startLevel(levelIndex);
|
||
else syncDom();
|
||
}
|
||
else if (act === 'campaign') {
|
||
const idx = pendingLevel;
|
||
pendingLevel = null;
|
||
suspended = null;
|
||
if (idx != null) startLevel(idx);
|
||
else syncDom();
|
||
} else syncDom();
|
||
}
|
||
// «Отмена»: возвращаем симуляцию, если она шла до открытия подтверждения завершения.
|
||
function cancelConfirm() {
|
||
const wasRunning = confirmResume;
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
pendingLevel = null;
|
||
if (wasRunning && state === 'PAUSED') resumeGame();
|
||
else syncDom();
|
||
}
|
||
|
||
// В меню (настройки). Прерванную партию не теряем — откладываем её в suspended.
|
||
function toMenu() {
|
||
suspendGame();
|
||
state = 'MENU';
|
||
mode = 'menu';
|
||
confirmAction = null;
|
||
confirmResume = false;
|
||
// Уровень кампании отложен в suspended целиком: активную кампанию в глобалах
|
||
// сбрасываем (вернётся через resumeFromMenu). Прогресс кампании сохраняется.
|
||
campaign = false;
|
||
levelIndex = -1;
|
||
zoneRect = null;
|
||
pendingLevel = null;
|
||
applyMenuSettings(); // активная игра возвращается к пользовательским настройкам (D1)
|
||
renderDirty = true; // маска MENU ([1,2]) отличается от боевых фаз
|
||
syncDom();
|
||
persist();
|
||
}
|
||
|
||
// ==== КНОПКИ ====
|
||
el.btnReady.addEventListener('click', commitTurn);
|
||
el.btnReset.addEventListener('click', resetGame);
|
||
el.btnPause.addEventListener('click', pauseGame);
|
||
el.speedRow.addEventListener('click', function (e) {
|
||
const b = e.target.closest('.speedbtn');
|
||
if (b) setSpeed(Number(b.dataset.speed));
|
||
});
|
||
el.btnEnd.addEventListener('click', requestEndGame);
|
||
el.btnEndPause.addEventListener('click', requestEndGame);
|
||
el.btnResume.addEventListener('click', resumeGame);
|
||
el.btnEditP1.addEventListener('click', function () { startEdit(1); });
|
||
el.btnEditP2.addEventListener('click', function () { startEdit(2); });
|
||
el.btnHandoff.addEventListener('click', handoffNext);
|
||
el.btnHandoffBack.addEventListener('click', handoffBack);
|
||
el.btnPlay.addEventListener('click', showTwoSetup);
|
||
el.btnRevenge.addEventListener('click', requestNewGame);
|
||
// Кампания: «Заново» перезапускает текущий уровень с начала (с подтверждением).
|
||
if (el.btnRestart) el.btnRestart.addEventListener('click', requestRestartLevel);
|
||
// Экран «Игра на двоих»: «Начать» = новая партия (с подтверждением, если есть отложенная),
|
||
// «В меню» = возврат на сплэш.
|
||
if (el.btnStartTwo) el.btnStartTwo.addEventListener('click', requestNewGame);
|
||
if (el.btnTwoBack) el.btnTwoBack.addEventListener('click', toMenu);
|
||
el.btnPauseMenu.addEventListener('click', toMenu);
|
||
el.btnGameoverMenu.addEventListener('click', toMenu);
|
||
if (el.btnContinue) el.btnContinue.addEventListener('click', resumeFromMenu);
|
||
// Кампания: вход/выход из выбора уровня, следующий уровень, «К уровням».
|
||
if (el.btnCampaign) el.btnCampaign.addEventListener('click', showLevelSelect);
|
||
if (el.btnLevelsBack) el.btnLevelsBack.addEventListener('click', toMenu);
|
||
if (el.btnNextLevel) el.btnNextLevel.addEventListener('click', function () { startLevel(levelIndex + 1); });
|
||
if (el.btnGameoverLevels) el.btnGameoverLevels.addEventListener('click', showLevelSelect);
|
||
if (el.btnConfirmOk) el.btnConfirmOk.addEventListener('click', confirmProceed);
|
||
if (el.btnConfirmCancel) el.btnConfirmCancel.addEventListener('click', cancelConfirm);
|
||
document.getElementById('btnSettings').addEventListener('click', toMenu);
|
||
Object.keys(TOOL_BTN).forEach(function (id) {
|
||
if (TOOL_BTN[id]) TOOL_BTN[id].addEventListener('click', function () { selectTool(id); });
|
||
});
|
||
if (el.optSize) {
|
||
el.optSize.addEventListener('change', function () {
|
||
const p = String(el.optSize.value).split('x'); // value = "WxH"
|
||
const w = parseInt(p[0], 10), h = parseInt(p[1], 10);
|
||
if (Number.isFinite(w) && Number.isFinite(h)) setFieldDim(w, h);
|
||
});
|
||
}
|
||
// Опция «Замкнутая карта (torus)».
|
||
if (el.optWrap) {
|
||
el.optWrap.addEventListener('change', function () { setWrap(el.optWrap.checked); });
|
||
}
|
||
// Выбор готового паттерна (штамп). Транзиентно (как speedMult) — в сейв не пишем;
|
||
// пустое значение → «нет паттерна» (stamp = null).
|
||
if (el.optPattern) {
|
||
el.optPattern.addEventListener('change', function () {
|
||
stamp = findPattern(el.optPattern.value) || null;
|
||
stampRot = 0; // новый паттерн — стартуем без поворота
|
||
wheelAccum = 0; // и без накопленной дельты колеса
|
||
syncDom();
|
||
});
|
||
// Подавляем нативный popup и открываем свой список (см. buildPatternMenu).
|
||
el.optPattern.addEventListener('mousedown', function (e) {
|
||
e.preventDefault();
|
||
togglePatternMenu();
|
||
});
|
||
}
|
||
// Выбор фигуры в своём списке: пишем значение в <select> и шлём change — единый путь
|
||
// установки stamp (см. обработчик change выше).
|
||
if (el.patternMenu) {
|
||
el.patternMenu.addEventListener('click', function (e) {
|
||
const b = e.target && e.target.closest ? e.target.closest('.patitem') : null;
|
||
if (!b) return;
|
||
if (el.optPattern) {
|
||
el.optPattern.value = b.getAttribute('data-id');
|
||
el.optPattern.dispatchEvent(new Event('change'));
|
||
}
|
||
closePatternMenu();
|
||
});
|
||
}
|
||
// Закрытие своего списка: клик вне, Escape, resize/scroll.
|
||
window.addEventListener('pointerdown', function (e) {
|
||
if (!patternMenuOpen()) return;
|
||
if (el.patternMenu.contains(e.target) || e.target === el.optPattern) return;
|
||
closePatternMenu();
|
||
}, true);
|
||
window.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && patternMenuOpen()) closePatternMenu();
|
||
});
|
||
window.addEventListener('resize', closePatternMenu);
|
||
// Прокрутку самого списка (#patternMenu) не считаем «уходом» — иначе колесо,
|
||
// которым листают фигуры, тут же закрывало бы список. Закрываем только при
|
||
// прокрутке внешних контейнеров (страница/#ui в боковой раскладке).
|
||
window.addEventListener('scroll', function (e) {
|
||
if (el.patternMenu && e.target && el.patternMenu.contains(e.target)) return;
|
||
closePatternMenu();
|
||
}, true);
|
||
// Поворот активного паттерна кнопкой «↻» (+90° за нажатие) — доступно и на тач-экранах,
|
||
// где нет колеса мыши. Колесо над доской по-прежнему работает (см. обработчик wheel).
|
||
if (el.btnRotate) {
|
||
el.btnRotate.addEventListener('click', function () {
|
||
if (!stamp) return;
|
||
stampRot = (stampRot + 1) % 4;
|
||
wheelAccum = 0;
|
||
syncDom();
|
||
});
|
||
}
|
||
// Тумблер режима «постановка по отпусканию» (тап). Транзиентно (в сейв не пишем).
|
||
if (el.btnTapPlace) {
|
||
el.btnTapPlace.addEventListener('click', function () {
|
||
tapPlace = !tapPlace;
|
||
pressing = false; // смена режима отменяет незавершённое удержание
|
||
syncDom();
|
||
});
|
||
}
|
||
// Тема оформления: селект в меню → applyTheme + сохранение.
|
||
if (el.optTheme) {
|
||
el.optTheme.addEventListener('change', function () { setTheme(el.optTheme.value); });
|
||
}
|
||
// Ввод бюджета: по change/blur применяем целое ≥1, чужое — игнорируем; поле нормализуем.
|
||
// Комбобокс: подсказки-пресеты (datalist #budgetOptions) не заменяют ввод произвольного числа.
|
||
if (el.optBudget) {
|
||
const applyBudget = function () {
|
||
const v = parseInt(el.optBudget.value, 10);
|
||
if (Number.isFinite(v) && v >= 1) setBudgetTotal(v);
|
||
el.optBudget.value = menuBudget;
|
||
syncDom();
|
||
};
|
||
el.optBudget.addEventListener('change', applyBudget);
|
||
el.optBudget.addEventListener('blur', applyBudget);
|
||
}
|
||
|
||
// ==== ТЕСТ-ФОРСЫ (часть архитектуры, выставляются на window) ====
|
||
// Форсировать состояние с корректной установкой производных (turn/mode/снимок).
|
||
function forceState(name) {
|
||
if (name === 'PLACE_P1' || name === 'EDIT_P1') { beginDraw(name, 1); }
|
||
else if (name === 'PLACE_P2' || name === 'EDIT_P2') { beginDraw(name, 2); }
|
||
else {
|
||
state = name;
|
||
mode = modeForState(name);
|
||
}
|
||
// Тест-форс устойчивости: на паузе/в дорисовке считаем поле устойчивым (дорисовка
|
||
// разрешена — тесты переходов не гоняются через реальную симуляцию); в RUNNING
|
||
// засеваем окно; в остальных фазах устойчивость не оценивалась.
|
||
if (name === 'RUNNING') seedStability();
|
||
else if (name === 'PAUSED' || name === 'EDIT_P1' || name === 'EDIT_P2') fieldStable = true;
|
||
else resetStability();
|
||
renderDirty = true; // смена маски/фазы
|
||
syncDom();
|
||
return state;
|
||
}
|
||
// Построить поле из ASCII-строк ('.'/'0' = мёртвая, '1'/'2'/'3' = цвета).
|
||
function setFieldASCII(str, w) {
|
||
let rows;
|
||
if (Array.isArray(str)) rows = str.slice();
|
||
else {
|
||
rows = String(str).split('\n');
|
||
if (rows.length && rows[rows.length - 1] === '') rows.pop();
|
||
}
|
||
const h = rows.length;
|
||
fieldW = w || (rows[0] ? rows[0].length : DEFAULT_FIELD);
|
||
fieldH = h;
|
||
field = new Uint8Array(fieldW * fieldH);
|
||
next = new Uint8Array(fieldW * fieldH);
|
||
for (let y = 0; y < h; y++) {
|
||
const line = String(rows[y]);
|
||
for (let x = 0; x < fieldW; x++) {
|
||
const ch = line[x] || '.';
|
||
const v = ch === '1' ? 1 : ch === '2' ? 2 : ch === '3' ? 3 : 0;
|
||
field[y * fieldW + x] = v;
|
||
}
|
||
}
|
||
turnStartField = field.slice();
|
||
// Согласованность снимков: бюджет старта = текущее «потрачено» (тест-форс).
|
||
turnStartBudget = { used1: budget.used1, used2: budget.used2 };
|
||
rebuildNeighborTables();
|
||
resetStability(); // новое поле — устойчивость не оценивалась
|
||
renderDirty = true; // новое поле/размер — пересобрать слой клеток
|
||
computeBoard();
|
||
syncDom();
|
||
return field;
|
||
}
|
||
|
||
// ==== СТАРТ ====
|
||
window.addEventListener('resize', resize);
|
||
window.addEventListener('contextmenu', function (e) { e.preventDefault(); }); // без меню на правом клике/лонгтапе
|
||
document.addEventListener('visibilitychange', function () {
|
||
// Авто-пауза при уходе со вкладки. Сейв при этом НЕ трогаем: последнее
|
||
// сохранённое состояние (например, RUNNING) должно пережить возврат/reload,
|
||
// иначе скрытие вкладки на миг превращало бы незавершённую партию в PAUSED.
|
||
if (document.hidden && state === 'RUNNING') { state = 'PAUSED'; mode = modeForState(state); syncDom(); }
|
||
});
|
||
canvas.addEventListener('pointerdown', onPointerDown);
|
||
canvas.addEventListener('pointermove', onPointerMove);
|
||
canvas.addEventListener('pointerup', onPointerUp);
|
||
canvas.addEventListener('pointercancel', onPointerUp);
|
||
canvas.addEventListener('pointerleave', function () { hoverCell = null; });
|
||
window.addEventListener('pointerup', onPointerUp);
|
||
// Колесо мыши над доской поворачивает активный паттерн на 90° за «щелчок» (stampRot 0..3,
|
||
// по часовой стрелке при прокрутке вниз). Действует только в фазах рисования и при выбранном
|
||
// штампе; вне этого — инертно (страница всё равно не скроллится: overflow:hidden).
|
||
// deltaY накапливается до порога WHEEL_ROT_STEP: мышь (один «щелчок» ≈ 100px) даёт один
|
||
// поворот, а тачпад с мелкими частыми дельтами — контролируемо, а не «крутится бешено».
|
||
// deltaMode нормализуем к пикселям (1 = строки, 2 = страницы). passive:false — чтобы
|
||
// preventDefault гасил инерционную прокрутку/зум.
|
||
const WHEEL_ROT_STEP = 100;
|
||
var wheelAccum = 0;
|
||
canvas.addEventListener('wheel', function (e) {
|
||
if (!isDrawState(state) || !stamp || !e.deltaY) { wheelAccum = 0; return; }
|
||
e.preventDefault();
|
||
const d = e.deltaY * (e.deltaMode === 1 ? 40 : e.deltaMode === 2 ? 400 : 1);
|
||
wheelAccum += d;
|
||
if (Math.abs(wheelAccum) < WHEEL_ROT_STEP) return;
|
||
stampRot = (stampRot + (wheelAccum > 0 ? 1 : 3)) % 4;
|
||
wheelAccum = 0;
|
||
syncDom();
|
||
}, { passive: false });
|
||
|
||
// ==== КЛАВИАТУРА ====
|
||
// Пробел ставит/снимает паузу (RUNNING↔PAUSED), как у медиаплеера. В остальных
|
||
// фазах (меню, расстановка, передача, финал) и при открытом диалоге подтверждения
|
||
// ничего не делаем. Фокус в интерактивном элементе (поле ввода/кнопка) не трогаем —
|
||
// пробел там имеет своё назначение (ввод, клик по кнопке).
|
||
window.addEventListener('keydown', function (e) {
|
||
if (e.repeat || e.ctrlKey || e.metaKey || e.altKey) return;
|
||
if (e.code !== 'Space' && e.key !== ' ' && e.key !== 'Spacebar') return;
|
||
const tag = e.target && e.target.tagName ? e.target.tagName.toLowerCase() : '';
|
||
if (tag === 'input' || tag === 'select' || tag === 'textarea' || tag === 'button' || tag === 'a') return;
|
||
if (confirmAction) return; // диалог подтверждения — не мешаем паузе/возврату
|
||
if (state === 'RUNNING') { e.preventDefault(); pauseGame(); }
|
||
else if (state === 'PAUSED') { e.preventDefault(); resumeGame(); }
|
||
});
|
||
|
||
// Константы в window (let/const не становятся свойствами window сами по себе).
|
||
window.SAVE_KEY = SAVE_KEY;
|
||
window.COLORS = COLORS;
|
||
window.TOOLS = TOOLS;
|
||
window.PATTERNS = PATTERNS;
|
||
window.FIELD_SIZES = FIELD_SIZES;
|
||
window.FIELD_DIMS = FIELD_DIMS;
|
||
window.STEPS_PER_SEC = STEPS_PER_SEC;
|
||
window.DEFAULT_BUDGET = DEFAULT_BUDGET;
|
||
window.CAMPAIGN_LEVELS = CAMPAIGN_LEVELS;
|
||
window.CAMPAIGN_PLAYER = CAMPAIGN_PLAYER;
|
||
window.CAMPAIGN_ENEMY = CAMPAIGN_ENEMY;
|
||
window.canvas = canvas;
|
||
window.ctx = ctx;
|
||
|
||
// Резюм прошлой партии из localStorage (или чистый старт в MENU).
|
||
// Настройки (размер/инструмент/скорость/wrap/бюджет) применяются к новой партии.
|
||
// applySave() сам пересобирает таблицы соседей под текущий размер/wrap.
|
||
applySave(loadSave());
|
||
|
||
resize();
|
||
syncDom();
|
||
draw();
|
||
requestAnimationFrame(frame);
|
||
</script>
|
||
</body>
|
||
</html>
|