SeaWolf-AI commited on
Commit
3aa55c7
·
verified ·
1 Parent(s): 7f7ce5f

Minesweeper preinstalled for every visitor

Browse files
Files changed (3) hide show
  1. common.js +1 -0
  2. mines.js +470 -469
  3. store.js +3 -2
common.js CHANGED
@@ -22,6 +22,7 @@ css.textContent = `@keyframes vkpulse{0%{box-shadow:0 0 0 0 rgba(99,102,241,.55)
22
  .dk.hl{position:relative}.dk.hl::after{content:'';position:absolute;top:4px;right:4px;width:8px;height:8px;border-radius:50%;background:#E11D48}
23
  @media (max-width:700px){.win .body{overflow-x:hidden}.win .body *{max-width:100%;box-sizing:border-box;min-width:0}.win .body pre,.win .body code{white-space:pre-wrap;word-break:break-word}.win .body table{display:block;overflow-x:auto}}
24
  #linkbar{display:flex;flex-wrap:nowrap;gap:6px;overflow-x:auto;scrollbar-width:none;padding:2px 0 4px;grid-column:1/-1;width:100%}#linkbar::-webkit-scrollbar{display:none}#linkbar a{flex:0 0 auto;font:700 11px/1 system-ui,sans-serif;color:#fff;background:var(--c);padding:4px 8px;border-radius:999px;text-decoration:none;white-space:nowrap}#linkbar a:hover{filter:brightness(1.15)}
 
25
  @media (prefers-reduced-motion:reduce){.icon.hl .g,.dk.hl .g{animation:none}}`;
26
  document.head.append(css);
27
  })();
 
22
  .dk.hl{position:relative}.dk.hl::after{content:'';position:absolute;top:4px;right:4px;width:8px;height:8px;border-radius:50%;background:#E11D48}
23
  @media (max-width:700px){.win .body{overflow-x:hidden}.win .body *{max-width:100%;box-sizing:border-box;min-width:0}.win .body pre,.win .body code{white-space:pre-wrap;word-break:break-word}.win .body table{display:block;overflow-x:auto}}
24
  #linkbar{display:flex;flex-wrap:nowrap;gap:6px;overflow-x:auto;scrollbar-width:none;padding:2px 0 4px;grid-column:1/-1;width:100%}#linkbar::-webkit-scrollbar{display:none}#linkbar a{flex:0 0 auto;font:700 11px/1 system-ui,sans-serif;color:#fff;background:var(--c);padding:4px 8px;border-radius:999px;text-decoration:none;white-space:nowrap}#linkbar a:hover{filter:brightness(1.15)}
25
+ .icon[data-open="mines"]{position:relative}.icon[data-open="mines"]::before{content:'from App Store';position:absolute;top:-2px;left:50%;transform:translateX(-50%);background:#4F46E5;color:#fff;font:700 8px/1 system-ui,sans-serif;padding:2px 4px;border-radius:5px;white-space:nowrap;z-index:1}
26
  @media (prefers-reduced-motion:reduce){.icon.hl .g,.dk.hl .g{animation:none}}`;
27
  document.head.append(css);
28
  })();
mines.js CHANGED
@@ -1,469 +1,470 @@
1
- /* ViKernel Desktop: Minesweeper (VDR app). Original code and art, drawn with CSS/SVG. */
2
- (function () {
3
- 'use strict';
4
-
5
- // ---------- i18n ----------
6
- var STR = {
7
- en: {
8
- mines_title: 'Minesweeper', mines_beginner: 'Beginner', mines_intermediate: 'Intermediate',
9
- mines_best: 'Best', mines_none: 'none', mines_new: 'New game', mines_hint: 'AI hint (Muse Spark)',
10
- mines_cheat: 'Try to cheat: read mine map', mines_agent: 'Agent and kernel', mines_audit: 'App audit',
11
- mines_win: 'Cleared! Time', mines_lose: 'Boom. All mines revealed.', mines_newbest: 'New best time!',
12
- mines_thinking: 'Muse Spark is reading the visible board...', mines_empty_agent: 'No agent activity yet. Ask for an AI hint to see the agent propose a tool call and the kernel decide.',
13
- mines_empty_audit: 'No verdicts recorded in this session yet.', mines_suggest: 'AI suggestion, may be wrong',
14
- mines_rate: 'Too many requests. Please wait a moment and try again.', mines_err: 'The request failed',
15
- mines_nohint: 'The model did not name a hidden cell this time.', mines_over: 'Start a new game to ask for a hint.',
16
- mines_about: 'About', mines_close: 'Close', mines_retry: 'Retry'
17
- },
18
- ko: {
19
- mines_title: '지뢰찾기', mines_beginner: '초급', mines_intermediate: '중급', mines_best: '최고', mines_none: '없음',
20
- mines_new: '새 게임', mines_hint: 'AI 힌트 (Muse Spark)', mines_cheat: '부정행위 시도: 지뢰 지도 읽기',
21
- mines_agent: '에이전트와 커널', mines_audit: '앱 감사 기록', mines_win: '성공! 시간', mines_lose: '펑. 모든 지뢰를 공개합니다.',
22
- mines_newbest: '최고 기록!', mines_thinking: 'Muse Spark 가 보이는 판을 읽는 중...',
23
- mines_empty_agent: '아직 에이전트 활동이 없습니다. AI 힌트를 요청해 보세요.', mines_empty_audit: '이 세션의 판정 기록이 아직 없습니다.',
24
- mines_suggest: 'AI 제안, 틀릴 수 있음', mines_rate: '요청이 너무 많습니다. 잠시 후 다시 시도하세요.', mines_err: '요청 실패',
25
- mines_nohint: '이번에는 모델이 숨은 칸을 지목하지 않았습니다.', mines_over: '힌트를 받으려면 새 게임을 시작하세요.',
26
- mines_about: '정보', mines_close: '닫기', mines_retry: '다시 시도'
27
- }
28
- };
29
- try {
30
- window.VK_I18N = window.VK_I18N || {};
31
- ['en', 'ko'].forEach(function (l) { window.VK_I18N[l] = Object.assign(window.VK_I18N[l] || {}, STR[l]); });
32
- } catch (e) { /* ignore */ }
33
-
34
- var MODES = {
35
- beginner: { rows: 9, cols: 9, mines: 10, key: 'mines_beginner' },
36
- intermediate: { rows: 16, cols: 16, mines: 40, key: 'mines_intermediate' }
37
- };
38
-
39
- function esc(s) {
40
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
41
- return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
42
- });
43
- }
44
- function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
45
- function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) { /* ignore */ } }
46
-
47
- // ---------- art (original) ----------
48
- var SVG_MINE = '<svg viewBox="0 0 20 20" aria-hidden="true"><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">' +
49
- '<path d="M10 2.5v3M10 14.5v3M2.5 10h3M14.5 10h3M4.8 4.8l2 2M13.2 13.2l2 2M15.2 4.8l-2 2M6.8 13.2l-2 2"/></g>' +
50
- '<rect x="5.6" y="5.6" width="8.8" height="8.8" rx="3" fill="currentColor"/><circle cx="8.6" cy="8.6" r="1.3" fill="#fff" opacity=".75"/></svg>';
51
- var SVG_FLAG = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M6 3v14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
52
- '<path d="M6.8 3.6l8.4 3.2-8.4 3.4z" fill="var(--mn-flag)"/><path d="M3.5 17h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>';
53
- function face(state) {
54
- var eyes = '<circle cx="9" cy="10.5" r="1.4"/><circle cx="15" cy="10.5" r="1.4"/>', mouth = '<path d="M8.5 15c2 2 5 2 7 0" fill="none"/>';
55
- if (state === 'tense') mouth = '<circle cx="12" cy="15.6" r="1.6" fill="none"/>';
56
- if (state === 'lost') { eyes = '<path d="M7.6 9.1l2.8 2.8M10.4 9.1l-2.8 2.8M13.6 9.1l2.8 2.8M16.4 9.1l-2.8 2.8" fill="none"/>'; mouth = '<path d="M8.5 16.5c2-1.8 5-1.8 7 0" fill="none"/>'; }
57
- if (state === 'won') { eyes = '<path d="M6.5 9.5h11M7 9.5c0 2.2 3.4 2.2 3.4 0M13.6 9.5c0 2.2 3.4 2.2 3.4 0" fill="none"/>'; mouth = '<path d="M8 14.6c2.4 2.8 5.6 2.8 8 0" fill="none"/>'; }
58
- return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2.5" y="2.5" width="19" height="19" rx="7" fill="var(--mn-face)" stroke="currentColor" stroke-width="1.4"/>' +
59
- '<g fill="currentColor" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">' + eyes + mouth + '</g></svg>';
60
- }
61
-
62
- // ---------- styles ----------
63
- var CSS = '' +
64
- '.mn{--mn-bg:#f6f8f7;--mn-panel:#fff;--mn-ink:#16211b;--mn-sub:#5b6b62;--mn-line:#dfe6e2;--mn-hid:#cfe3d6;--mn-hid2:#b9d6c3;--mn-open:#fbfdfc;' +
65
- '--mn-acc:#16A34A;--mn-flag:#e4572e;--mn-face:#ffd76a;--mn-boom:#fde2dc;--mn-ok:#15803d;--mn-bad:#dc2626;' +
66
- '--n1:#2563eb;--n2:#0f8a5f;--n3:#d9480f;--n4:#7c3aed;--n5:#b42318;--n6:#0e7490;--n7:#334155;--n8:#a16207;' +
67
- 'display:flex;flex-direction:column;height:100%;background:var(--mn-bg);color:var(--mn-ink);font:13px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;box-sizing:border-box;position:relative;overflow:hidden}' +
68
- '@media (prefers-color-scheme:dark){.mn{--mn-bg:#101613;--mn-panel:#17201b;--mn-ink:#e6efe9;--mn-sub:#93a59a;--mn-line:#26332c;--mn-hid:#2a3d32;--mn-hid2:#335042;--mn-open:#141c18;' +
69
- '--mn-face:#e8bf4c;--mn-boom:#4a1f1a;--n1:#7aa7ff;--n2:#4fd19c;--n3:#ff9b5e;--n4:#c4a1ff;--n5:#ff7a73;--n6:#5fd3e6;--n7:#cbd5e1;--n8:#f1c84b}}' +
70
- '.mn *{box-sizing:border-box}.mn button{font:inherit;color:inherit}' +
71
- '.mn-strip{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:6px 12px;background:color-mix(in srgb,var(--mn-acc) 12%,var(--mn-panel));border-bottom:1px solid var(--mn-line);font-size:11.5px;color:var(--mn-sub)}' +
72
- '.mn-strip b{color:var(--mn-acc)}.mn-strip .mn-i{margin-left:auto}' +
73
- '.mn-i{width:24px;height:24px;border-radius:50%;border:1px solid var(--mn-line);background:var(--mn-panel);cursor:pointer;font-weight:700;font-style:italic;font-family:Georgia,serif}' +
74
- '.mn-main{flex:1;display:flex;gap:12px;padding:12px;min-height:0}' +
75
- '.mn-left{flex:1 1 auto;display:flex;flex-direction:column;gap:10px;min-width:0;min-height:0}' +
76
- '.mn-side{flex:0 0 230px;display:flex;flex-direction:column;gap:10px;min-height:0;overflow:auto}' +
77
- '.mn.narrow .mn-main{flex-direction:column;overflow:auto}.mn.narrow .mn-side{flex:0 0 auto;overflow:visible}.mn.narrow .mn-left{flex:0 0 auto}' +
78
- '.mn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}' +
79
- '.mn-seg{display:inline-flex;border:1px solid var(--mn-line);border-radius:9px;overflow:hidden;background:var(--mn-panel)}' +
80
- '.mn-seg button{border:0;background:none;padding:5px 10px;cursor:pointer}.mn-seg button[aria-pressed=true]{background:var(--mn-acc);color:#fff}' +
81
- '.mn-lcd{font:600 15px ui-monospace,Consolas,monospace;padding:4px 9px;border-radius:8px;background:var(--mn-panel);border:1px solid var(--mn-line);min-width:52px;text-align:center}' +
82
- '.mn-face{width:38px;height:38px;border-radius:11px;border:1px solid var(--mn-line);background:var(--mn-panel);cursor:pointer;padding:3px}.mn-face svg{width:100%;height:100%}' +
83
- '.mn-face:active{transform:scale(.95)}.mn-best{color:var(--mn-sub);font-size:12px}.mn-sp{flex:1}' +
84
- '.mn-wrap{flex:1;min-height:180px;display:flex;align-items:center;justify-content:center;position:relative}' +
85
- '.mn-grid{display:grid;gap:2px;padding:6px;border-radius:12px;background:var(--mn-line);outline:none;touch-action:manipulation;user-select:none;-webkit-user-select:none}' +
86
- '.mn-grid:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--mn-acc) 45%,transparent)}' +
87
- '.mn-c{width:var(--cs);height:var(--cs);border:0;border-radius:calc(var(--cs)*.18);padding:0;display:flex;align-items:center;justify-content:center;cursor:pointer;' +
88
- 'font:800 calc(var(--cs)*.55)/1 ui-rounded,system-ui,sans-serif;background:linear-gradient(160deg,var(--mn-hid),var(--mn-hid2));position:relative;-webkit-touch-callout:none}' +
89
- '.mn-c svg{width:68%;height:68%}.mn-c.o{background:var(--mn-open);cursor:default}.mn-c.boom{background:var(--mn-boom)}.mn-c.bad::after{content:"";position:absolute;inset:18%;' +
90
- 'background:linear-gradient(45deg,transparent 45%,var(--mn-bad) 45% 55%,transparent 55%)}' +
91
- '.mn-c.foc{outline:2px solid var(--mn-acc);outline-offset:-2px;z-index:1}' +
92
- '.mn-c.hint::before{content:"";position:absolute;inset:-3px;border-radius:inherit;border:3px solid #f59e0b;animation:mnp 1.1s ease-in-out infinite}' +
93
- '@keyframes mnp{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.18);opacity:.35}}' +
94
- '.mn-c[data-n="1"]{color:var(--n1)}.mn-c[data-n="2"]{color:var(--n2)}.mn-c[data-n="3"]{color:var(--n3)}.mn-c[data-n="4"]{color:var(--n4)}' +
95
- '.mn-c[data-n="5"]{color:var(--n5)}.mn-c[data-n="6"]{color:var(--n6)}.mn-c[data-n="7"]{color:var(--n7)}.mn-c[data-n="8"]{color:var(--n8)}' +
96
- '.mn-status{min-height:20px;text-align:center;font-weight:600}.mn-status.win{color:var(--mn-ok)}.mn-status.lose{color:var(--mn-bad)}' +
97
- '.mn-hintlbl{font-size:12px;color:#b45309;text-align:center;min-height:16px}' +
98
- '.mn-actions{display:flex;flex-direction:column;gap:6px}' +
99
- '.mn-btn{display:flex;align-items:center;justify-content:center;gap:7px;padding:8px 10px;border-radius:9px;border:1px solid var(--mn-line);background:var(--mn-panel);cursor:pointer;font-weight:600}' +
100
- '.mn-btn.pri{background:var(--mn-acc);border-color:var(--mn-acc);color:#fff}.mn-btn.warn{border-color:color-mix(in srgb,var(--mn-bad) 45%,var(--mn-line));color:var(--mn-bad)}' +
101
- '.mn-btn:disabled{opacity:.6;cursor:wait}.mn-spin{width:13px;height:13px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:mns .7s linear infinite}' +
102
- '@keyframes mns{to{transform:rotate(360deg)}}' +
103
- '.mn-card{background:var(--mn-panel);border:1px solid var(--mn-line);border-radius:11px;padding:10px}' +
104
- '.mn-card h4{margin:0 0 7px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:var(--mn-sub)}' +
105
- '.mn-ev{border-left:3px solid var(--mn-line);padding:4px 0 4px 8px;margin:6px 0;word-break:break-word}.mn-ev code{font-size:11.5px}' +
106
- '.mn-ev small{display:block;color:var(--mn-sub)}.mn-empty{color:var(--mn-sub);font-size:12px}.mn-errbox{color:var(--mn-bad);font-size:12px}' +
107
- '.mn-tag{display:inline-block;padding:1px 7px;border-radius:6px;font-size:11px;font-weight:800;color:#fff;margin-right:5px}' +
108
- '.mn-tag.allow{background:var(--mn-ok)}.mn-tag.deny{background:var(--mn-bad)}' +
109
- '.mn-audit{list-style:none;margin:0;padding:0;font-size:12px}.mn-audit li{display:flex;gap:6px;align-items:center;padding:3px 0;border-bottom:1px dashed var(--mn-line)}' +
110
- '.mn-audit li:last-child{border:0}.mn-audit span.k{color:var(--mn-sub);margin-left:auto;font-family:ui-monospace,monospace}' +
111
- '.mn-deny{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(420px,92%);z-index:20;background:var(--mn-panel);border:2px solid var(--mn-bad);' +
112
- 'border-radius:16px;padding:18px;box-shadow:0 18px 50px rgba(0,0,0,.3);animation:mnin .18s ease-out}' +
113
- '@keyframes mnin{from{transform:translate(-50%,-46%);opacity:0}}' +
114
- '.mn-deny .big{font-size:26px;font-weight:900;color:var(--mn-bad);letter-spacing:.06em}.mn-deny p{margin:8px 0}.mn-deny code{word-break:break-all}' +
115
- '.mn-over{position:absolute;inset:0;background:rgba(0,0,0,.35);z-index:19}' +
116
- '.mn-about{position:absolute;inset:24px;z-index:20;background:var(--mn-panel);border:1px solid var(--mn-line);border-radius:14px;padding:18px;overflow:auto;box-shadow:0 18px 50px rgba(0,0,0,.3)}' +
117
- '.mn-about h3{margin:0 0 8px}.mn-about ul{padding-left:18px}';
118
-
119
- function injectCSS() {
120
- if (document.getElementById('vk-mines-css')) return;
121
- var s = document.createElement('style'); s.id = 'vk-mines-css'; s.textContent = CSS; document.head.appendChild(s);
122
- }
123
-
124
- var ICON = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
125
- '<rect x="7" y="7" width="10" height="10" rx="3.5"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2 2M16.7 16.7l2 2M18.7 5.3l-2 2M7.3 16.7l-2 2"/></svg>';
126
-
127
- // session-wide audit (persists across window reopen in this page load)
128
- var AUDIT = [];
129
-
130
- window.VK_MINES_APP = function (h) {
131
- h = h || {};
132
- function T(k) { try { var r = h.t && h.t(k); if (r && r !== k) return r; } catch (e) { /* ignore */ } return STR.en[k] || k; }
133
- function toast(m) { try { if (h.toast) h.toast(m); } catch (e) { /* ignore */ } }
134
-
135
- return {
136
- id: 'mines', name: function () { return 'Minesweeper'; }, color: '#16A34A', icon: ICON, w: 560, h: 680,
137
- render: function (body) {
138
- injectCSS();
139
- body.innerHTML = '';
140
- var root = document.createElement('div'); root.className = 'mn';
141
- root.innerHTML =
142
- '<div class="mn-strip"><b>VDR app</b><span>·</span><span>Verified by ViKernel</span><span>·</span><span>declared scope: read game/visible/ only</span><span>·</span><span>no network</span>' +
143
- '<button class="mn-i" data-a="about" title="' + esc(T('mines_about')) + '" aria-label="' + esc(T('mines_about')) + '">i</button></div>' +
144
- '<div class="mn-main"><div class="mn-left">' +
145
- '<div class="mn-head"><div class="mn-seg" role="group">' +
146
- '<button data-mode="beginner">' + esc(T('mines_beginner')) + '</button><button data-mode="intermediate">' + esc(T('mines_intermediate')) + '</button></div>' +
147
- '<span class="mn-best"></span></div>' +
148
- '<div class="mn-head"><span class="mn-lcd" data-r="count" title="Mines left">0</span><span class="mn-sp"></span>' +
149
- '<button class="mn-face" data-a="new" title="' + esc(T('mines_new')) + ' (N)" aria-label="' + esc(T('mines_new')) + '"></button>' +
150
- '<span class="mn-sp"></span><span class="mn-lcd" data-r="time" title="Time">0</span></div>' +
151
- '<div class="mn-wrap"><div class="mn-grid" role="grid" tabindex="0" aria-label="Minefield"></div></div>' +
152
- '<div class="mn-status" aria-live="polite"></div><div class="mn-hintlbl"></div></div>' +
153
- '<div class="mn-side"><div class="mn-actions">' +
154
- '<button class="mn-btn pri" data-a="hint">' + esc(T('mines_hint')) + '</button>' +
155
- '<button class="mn-btn warn" data-a="cheat">' + esc(T('mines_cheat')) + '</button></div>' +
156
- '<div class="mn-card"><h4>' + esc(T('mines_agent')) + '</h4><div data-r="agent"></div></div>' +
157
- '<div class="mn-card"><h4>' + esc(T('mines_audit')) + '</h4><ul class="mn-audit" data-r="audit"></ul></div>' +
158
- '</div></div>';
159
- body.appendChild(root);
160
-
161
- var q = function (s) { return root.querySelector(s); };
162
- var gridEl = q('.mn-grid'), wrap = q('.mn-wrap'), faceBtn = q('.mn-face'), countEl = q('[data-r=count]'), timeEl = q('[data-r=time]');
163
- var statusEl = q('.mn-status'), hintLbl = q('.mn-hintlbl'), bestEl = q('.mn-best'), agentEl = q('[data-r=agent]'), auditEl = q('[data-r=audit]');
164
- var hintBtn = q('[data-a=hint]'), cheatBtn = q('[data-a=cheat]');
165
-
166
- var mode = lsGet('vk_mines_mode') === 'intermediate' ? 'intermediate' : 'beginner';
167
- var R, C, M, mine, open, flag, adj, cells, started, over, won, t0, elapsed, timer, focus = 0, hintIdx = -1, opened;
168
-
169
- // ---------- game core ----------
170
- function nbrs(i) {
171
- var r = (i / C) | 0, c = i % C, out = [];
172
- for (var dr = -1; dr <= 1; dr++) for (var dc = -1; dc <= 1; dc++) {
173
- if (!dr && !dc) continue; var rr = r + dr, cc = c + dc;
174
- if (rr >= 0 && rr < R && cc >= 0 && cc < C) out.push(rr * C + cc);
175
- }
176
- return out;
177
- }
178
- function newGame(m) {
179
- if (m) { mode = m; lsSet('vk_mines_mode', m); }
180
- var cfg = MODES[mode]; R = cfg.rows; C = cfg.cols; M = cfg.mines;
181
- var n = R * C; mine = new Array(n).fill(false); open = new Array(n).fill(false); flag = new Array(n).fill(false); adj = new Array(n).fill(0);
182
- started = false; over = false; won = false; elapsed = 0; opened = 0; hintIdx = -1; focus = Math.min(focus, n - 1);
183
- stopTimer(); statusEl.textContent = ''; statusEl.className = 'mn-status'; hintLbl.textContent = '';
184
- root.querySelectorAll('.mn-seg button').forEach(function (b) { b.setAttribute('aria-pressed', String(b.dataset.mode === mode)); });
185
- buildGrid(); updateHud(); fit();
186
- }
187
- function plant(safe) {
188
- var ban = {}; ban[safe] = 1; nbrs(safe).forEach(function (j) { ban[j] = 1; });
189
- var pool = []; for (var i = 0; i < R * C; i++) if (!ban[i]) pool.push(i);
190
- for (var k = 0; k < M; k++) { var j = k + Math.floor(Math.random() * (pool.length - k)); var tmp = pool[k]; pool[k] = pool[j]; pool[j] = tmp; mine[pool[k]] = true; }
191
- for (var x = 0; x < R * C; x++) adj[x] = nbrs(x).filter(function (y) { return mine[y]; }).length;
192
- }
193
- function reveal(i) {
194
- if (over || open[i] || flag[i]) return;
195
- if (!started) { plant(i); started = true; t0 = Date.now(); timer = setInterval(tick, 250); }
196
- if (mine[i]) { lose(i); return; }
197
- var stack = [i];
198
- while (stack.length) {
199
- var k = stack.pop(); if (open[k] || flag[k]) continue;
200
- open[k] = true; opened++; paint(k);
201
- if (adj[k] === 0) nbrs(k).forEach(function (j) { if (!open[j] && !mine[j]) stack.push(j); });
202
- }
203
- clearHint(); checkWin(); updateHud();
204
- }
205
- function chord(i) {
206
- if (over || !open[i] || !adj[i]) return;
207
- var ns = nbrs(i), f = ns.filter(function (j) { return flag[j]; }).length;
208
- if (f !== adj[i]) return;
209
- ns.forEach(function (j) { if (!open[j] && !flag[j]) reveal(j); });
210
- }
211
- function toggleFlag(i) {
212
- if (over || open[i]) return;
213
- flag[i] = !flag[i]; paint(i); updateHud();
214
- }
215
- function act(i) { if (open[i]) chord(i); else reveal(i); }
216
- function lose(i) {
217
- over = true; stopTimer();
218
- for (var k = 0; k < R * C; k++) {
219
- if (mine[k] && !flag[k]) { open[k] = true; paint(k); }
220
- else if (flag[k] && !mine[k]) cells[k].classList.add('bad');
221
- }
222
- cells[i].classList.add('boom'); clearHint();
223
- statusEl.textContent = T('mines_lose'); statusEl.className = 'mn-status lose'; updateHud();
224
- }
225
- function checkWin() {
226
- if (over || opened !== R * C - M) return;
227
- over = true; won = true; stopTimer();
228
- for (var k = 0; k < R * C; k++) if (mine[k] && !flag[k]) { flag[k] = true; paint(k); }
229
- var secs = Math.round(elapsed / 10) / 100, key = 'vk_mines_best_' + mode, prev = parseFloat(lsGet(key));
230
- var msg = T('mines_win') + ' ' + secs.toFixed(1) + 's';
231
- if (!(prev > 0) || secs < prev) { lsSet(key, String(secs)); msg += '. ' + T('mines_newbest'); }
232
- statusEl.textContent = msg; statusEl.className = 'mn-status win'; toast(msg);
233
- }
234
- function tick() {
235
- if (!root.isConnected) { stopTimer(); return; }
236
- elapsed = Date.now() - t0; timeEl.textContent = String(Math.min(999, Math.floor(elapsed / 1000)));
237
- }
238
- function stopTimer() { if (timer) { clearInterval(timer); timer = null; if (started) elapsed = Date.now() - t0; } }
239
- function updateHud() {
240
- var f = flag.filter(Boolean).length; countEl.textContent = String(M - f);
241
- if (!started) timeEl.textContent = '0';
242
- faceBtn.innerHTML = face(over ? (won ? 'won' : 'lost') : 'ok');
243
- var b = parseFloat(lsGet('vk_mines_best_' + mode));
244
- bestEl.textContent = T('mines_best') + ' (' + T(MODES[mode].key) + '): ' + (b > 0 ? b.toFixed(1) + 's' : T('mines_none'));
245
- }
246
-
247
- // ---------- rendering ----------
248
- function buildGrid() {
249
- gridEl.innerHTML = ''; gridEl.style.gridTemplateColumns = 'repeat(' + C + ',var(--cs))'; cells = [];
250
- var frag = document.createDocumentFragment();
251
- for (var i = 0; i < R * C; i++) {
252
- var b = document.createElement('div'); b.className = 'mn-c'; b.dataset.i = i; b.setAttribute('role', 'gridcell');
253
- frag.appendChild(b); cells.push(b); paint(i);
254
- }
255
- gridEl.appendChild(frag); setFocus(focus, false);
256
- }
257
- function paint(i) {
258
- var el = cells[i]; if (!el) return;
259
- var r = (i / C) | 0, c = i % C, label = 'row ' + r + ' col ' + c + ': ';
260
- el.classList.toggle('o', open[i]); el.removeAttribute('data-n');
261
- if (open[i] && mine[i]) { el.innerHTML = SVG_MINE; label += 'mine'; }
262
- else if (open[i]) { el.innerHTML = adj[i] ? String(adj[i]) : ''; if (adj[i]) el.dataset.n = adj[i]; label += adj[i] || 'empty'; }
263
- else if (flag[i]) { el.innerHTML = SVG_FLAG; label += 'flagged'; }
264
- else { el.innerHTML = ''; label += 'hidden'; }
265
- el.setAttribute('aria-label', label);
266
- }
267
- function setFocus(i, scroll) {
268
- if (cells[focus]) cells[focus].classList.remove('foc');
269
- focus = Math.max(0, Math.min(R * C - 1, i));
270
- if (cells[focus]) { cells[focus].classList.add('foc'); gridEl.setAttribute('aria-activedescendant', ''); if (scroll) cells[focus].scrollIntoView({ block: 'nearest' }); }
271
- }
272
- function fit() {
273
- var narrow = root.clientWidth < 600; root.classList.toggle('narrow', narrow);
274
- var w = wrap.clientWidth - 14, hh = narrow ? Math.min(window.innerHeight * 0.6, w) : wrap.clientHeight - 14;
275
- var cs = Math.floor(Math.min((w - (C - 1) * 2) / C, (hh - (R - 1) * 2) / R));
276
- cs = Math.max(narrow ? 20 : 16, Math.min(46, cs || 24));
277
- gridEl.style.setProperty('--cs', cs + 'px');
278
- }
279
- var ro = window.ResizeObserver ? new ResizeObserver(fit) : null;
280
- if (ro) ro.observe(root); else window.addEventListener('resize', fit);
281
-
282
- // ---------- input ----------
283
- var lpTimer = null, lpFired = false, lastTouch = 0;
284
- function idxOf(e) { var t = e.target.closest && e.target.closest('.mn-c'); return t ? +t.dataset.i : -1; }
285
- gridEl.addEventListener('pointerdown', function (e) {
286
- var i = idxOf(e); if (i < 0) return;
287
- lpFired = false; if (!over && !open[i]) faceBtn.innerHTML = face('tense');
288
- if (e.pointerType === 'touch') {
289
- lastTouch = Date.now(); clearTimeout(lpTimer);
290
- lpTimer = setTimeout(function () { lpFired = true; toggleFlag(i); if (navigator.vibrate) navigator.vibrate(15); }, 400);
291
- }
292
- });
293
- function cancelLp() { clearTimeout(lpTimer); updateHud(); }
294
- gridEl.addEventListener('pointerup', cancelLp);
295
- gridEl.addEventListener('pointercancel', cancelLp);
296
- gridEl.addEventListener('pointerleave', cancelLp);
297
- gridEl.addEventListener('click', function (e) {
298
- var i = idxOf(e); if (i < 0) return;
299
- if (lpFired) { lpFired = false; return; }
300
- setFocus(i, false);
301
- if (e.shiftKey) toggleFlag(i); else act(i);
302
- });
303
- gridEl.addEventListener('contextmenu', function (e) {
304
- e.preventDefault(); var i = idxOf(e); if (i < 0) return;
305
- if (Date.now() - lastTouch < 800) return; // touch handled by long-press
306
- setFocus(i, false); toggleFlag(i);
307
- });
308
- gridEl.addEventListener('keydown', function (e) {
309
- var k = e.key, r = (focus / C) | 0, c = focus % C;
310
- if (k === 'ArrowUp') setFocus(Math.max(0, r - 1) * C + c, true);
311
- else if (k === 'ArrowDown') setFocus(Math.min(R - 1, r + 1) * C + c, true);
312
- else if (k === 'ArrowLeft') setFocus(r * C + Math.max(0, c - 1), true);
313
- else if (k === 'ArrowRight') setFocus(r * C + Math.min(C - 1, c + 1), true);
314
- else if (k === ' ' || k === 'Enter') act(focus);
315
- else if (k === 'f' || k === 'F') toggleFlag(focus);
316
- else if (k === 'n' || k === 'N') newGame();
317
- else return;
318
- e.preventDefault();
319
- });
320
- root.querySelectorAll('.mn-seg button').forEach(function (b) {
321
- b.addEventListener('click', function () { newGame(b.dataset.mode); gridEl.focus(); });
322
- });
323
- faceBtn.addEventListener('click', function () { newGame(); gridEl.focus(); });
324
-
325
- // ---------- ViKernel features ----------
326
- function boardText() {
327
- var lines = ['rows ' + R + ' cols ' + C];
328
- for (var r = 0; r < R; r++) {
329
- var s = '';
330
- for (var c = 0; c < C; c++) { var i = r * C + c; s += open[i] ? (mine[i] ? '#' : String(adj[i])) : (flag[i] ? 'F' : '#'); }
331
- lines.push(s);
332
- }
333
- return lines.join('\n'); // visible state only: mine positions are never sent
334
- }
335
- function clearHint() {
336
- if (hintIdx >= 0 && cells[hintIdx]) cells[hintIdx].classList.remove('hint');
337
- hintIdx = -1; hintLbl.textContent = '';
338
- }
339
- function tag(v) { var a = String(v || '').toUpperCase() === 'ALLOW'; return '<span class="mn-tag ' + (a ? 'allow' : 'deny') + '">' + (a ? 'ALLOW' : 'DENY') + '</span>'; }
340
- function renderAudit() {
341
- if (!AUDIT.length) { auditEl.innerHTML = '<li class="mn-empty">' + esc(T('mines_empty_audit')) + '</li>'; return; }
342
- auditEl.innerHTML = AUDIT.slice().reverse().map(function (a) {
343
- return '<li>' + tag(a.verdict) + '<span>' + esc(a.kind) + '</span><span class="k">' + (a.seq != null ? 'audit #' + esc(a.seq) : esc(a.time)) + '</span></li>';
344
- }).join('');
345
- }
346
- function addAudit(kind, verdict, seq) {
347
- AUDIT.push({ kind: kind, verdict: verdict, seq: seq, time: new Date().toLocaleTimeString() }); renderAudit();
348
- }
349
- function agentEmpty() { agentEl.innerHTML = '<div class="mn-empty">' + esc(T('mines_empty_agent')) + '</div>'; }
350
- function errMsg(e) { return e && e.message === 'rate' ? T('mines_rate') : T('mines_err') + ': ' + (e && e.message || 'unknown'); }
351
- function setBusy(btn, on, label) {
352
- btn.disabled = on; btn.innerHTML = on ? '<span class="mn-spin"></span>' + esc(label) : esc(label);
353
- }
354
-
355
- function renderEvents(evs) {
356
- var html = '';
357
- evs.forEach(function (ev) {
358
- if (ev.type === 'proposed') {
359
- html += '<div class="mn-ev"><b>Proposed</b> <code>' + esc(ev.fn) + '(' + esc(ev.args && ev.args.path ? ev.args.path : JSON.stringify(ev.args || {})) + ')</code>' +
360
- '<small>' + esc(ev.model || 'model') + (ev.model_ms != null ? ' · ' + esc(ev.model_ms) + ' ms' : '') + '</small></div>';
361
- } else if (ev.type === 'verdict') {
362
- html += '<div class="mn-ev">' + tag(ev.verdict) + (ev.audit_seq != null ? '<b>audit #' + esc(ev.audit_seq) + '</b>' : '') +
363
- '<small>rule ' + esc(ev.rule || '-') + (ev.reason ? ' · ' + esc(ev.reason) : '') + '</small></div>';
364
- } else if (ev.type === 'final') {
365
- html += '<div class="mn-ev"><b>Final</b><div>' + esc(ev.text) + '</div></div>';
366
- }
367
- });
368
- agentEl.innerHTML = html || '<div class="mn-empty">' + esc(T('mines_nohint')) + '</div>';
369
- }
370
-
371
- hintBtn.addEventListener('click', async function () {
372
- if (over) { toast(T('mines_over')); hintLbl.textContent = T('mines_over'); return; }
373
- clearHint(); setBusy(hintBtn, true, T('mines_hint'));
374
- agentEl.innerHTML = '<div class="mn-empty"><span class="mn-spin" style="display:inline-block;vertical-align:-2px;margin-right:6px"></span>' + esc(T('mines_thinking')) + '</div>';
375
- try {
376
- var res = await window.VK_API('game/hint', { session: window.VK_SID, board: boardText() });
377
- var evs = (res && res.events) || [];
378
- renderEvents(evs);
379
- var final = '';
380
- evs.forEach(function (ev) {
381
- if (ev.type === 'verdict') addAudit('hint: ' + (ev.fn || 'read_file'), ev.verdict, ev.audit_seq);
382
- if (ev.type === 'final') final = ev.text || '';
383
- });
384
- var m = /(\d+)\s*,\s*(\d+)/.exec(final);
385
- if (m) {
386
- var r = +m[1], c = +m[2], i = r * C + c;
387
- if (r < R && c < C && !open[i] && !flag[i] && !over) {
388
- hintIdx = i; cells[i].classList.add('hint'); setFocus(i, true);
389
- hintLbl.textContent = T('mines_suggest') + ' (row ' + r + ', col ' + c + ')'; return;
390
- }
391
- }
392
- hintLbl.textContent = T('mines_nohint');
393
- } catch (e) {
394
- agentEl.innerHTML = '<div class="mn-errbox">' + esc(errMsg(e)) + '</div><button class="mn-btn" data-a="retry" style="margin-top:6px;width:100%">' + esc(T('mines_retry')) + '</button>';
395
- var rb = agentEl.querySelector('[data-a=retry]'); rb.addEventListener('click', function () { hintBtn.click(); });
396
- } finally { setBusy(hintBtn, false, T('mines_hint')); }
397
- });
398
-
399
- var overlay = null;
400
- function closeOverlay() { if (overlay) { overlay.forEach(function (n) { n.remove(); }); overlay = null; gridEl.focus(); } }
401
- function showOverlay(cls, html) {
402
- closeOverlay();
403
- var bg = document.createElement('div'); bg.className = 'mn-over'; bg.addEventListener('click', closeOverlay);
404
- var box = document.createElement('div'); box.className = cls; box.setAttribute('role', 'dialog'); box.innerHTML = html;
405
- root.appendChild(bg); root.appendChild(box); overlay = [bg, box];
406
- var cb = box.querySelector('[data-a=close]'); if (cb) { cb.addEventListener('click', closeOverlay); cb.focus(); }
407
- }
408
- root.addEventListener('keydown', function (e) { if (e.key === 'Escape' && overlay) { closeOverlay(); e.stopPropagation(); } });
409
-
410
- cheatBtn.addEventListener('click', async function () {
411
- setBusy(cheatBtn, true, T('mines_cheat'));
412
- var ctl = null, done = false;
413
- try {
414
- var res = await Promise.race([
415
- window.VK_API('game/cheat', {}),
416
- new Promise(function (_, rej) { ctl = setTimeout(function () { if (!done) rej(new Error('timeout')); }, 8000); })
417
- ]);
418
- done = true; clearTimeout(ctl);
419
- var path = (res && res.args && res.args.path) || 'game/secret/mines.map';
420
- var v = String(res && res.verdict || 'DENY').toUpperCase();
421
- addAudit('cheat: ' + (res && res.fn || 'read_file'), v, res && res.audit_seq);
422
- showOverlay('mn-deny',
423
- '<div class="big">' + (v === 'ALLOW' ? 'ALLOW' : 'DENY') + '</div>' +
424
- '<p><b>DENY · rule ' + esc(res && res.rule || 'manifest') + ' · path ' + esc(path) + ' is outside the declared scope (game/visible/)</b></p>' +
425
- '<p>Call: <code>' + esc(res && res.fn || 'read_file') + '("' + esc(path) + '")</code>' + (res && res.app ? ' by <code>' + esc(res.app) + '</code>' : '') + '</p>' +
426
- (res && res.reason ? '<p>Reason (manifest rule): ' + esc(res.reason) + '</p>' : '') +
427
- (res && res.audit_seq != null ? '<p>Recorded as audit #' + esc(res.audit_seq) + '.</p>' : '') +
428
- '<p style="color:var(--mn-sub)">The app never saw the mine map. Your game is unchanged.</p>' +
429
- '<button class="mn-btn" data-a="close" style="width:100%">' + esc(T('mines_close')) + '</button>');
430
- } catch (e) {
431
- done = true; clearTimeout(ctl);
432
- showOverlay('mn-deny', '<div class="big" style="font-size:18px">' + esc(T('mines_err')) + '</div><p>' + esc(errMsg(e)) + '</p>' +
433
- '<button class="mn-btn" data-a="close" style="width:100%">' + esc(T('mines_close')) + '</button>');
434
- } finally { setBusy(cheatBtn, false, T('mines_cheat')); }
435
- });
436
-
437
- q('[data-a=about]').addEventListener('click', function () {
438
- showOverlay('mn-about',
439
- '<h3>Minesweeper for ViKernel Desktop</h3>' +
440
- '<p>Clear every safe cell without opening a mine. Numbers show how many mines touch a cell. The first click is always safe.</p>' +
441
- '<ul><li>Click or tap: reveal. Click a number with matching flags around it to open its neighbors.</li>' +
442
- '<li>Right-click, Shift+click or long-press: flag.</li><li>Keys: arrows move, Space or Enter reveals, F flags, N starts a new game.</li></ul>' +
443
- '<h3>Declared scope</h3>' +
444
- '<p>This is a VDR app verified by ViKernel. Its manifest declares one permission: read files under <code>game/visible/</code>. It makes no network calls of its own. ' +
445
- 'The AI hint sends only the visible board (hidden cells as #, flags as F, revealed numbers). Mine positions stay inside this window and are never shared.</p>' +
446
- '<p>"Try to cheat" asks the kernel to read <code>game/secret/mines.map</code>. The path is outside the declared scope, so the kernel denies it and logs the attempt in the audit list.</p>' +
447
- '<p>AI hints come from Muse Spark and may be wrong. Hints are highlighted, never played for you.</p>' +
448
- '<button class="mn-btn" data-a="close">' + esc(T('mines_close')) + '</button>');
449
- });
450
-
451
- agentEmpty(); renderAudit(); newGame();
452
- setTimeout(fit, 0);
453
- }
454
- };
455
- };
456
-
457
- // ---------- install ----------
458
- window.VK_MINES_INSTALL = function () {
459
- lsSet('vk_mines_installed', '1');
460
- if (window.VK_HX_ADD) window.VK_HX_ADD(window.VK_MINES_APP);
461
- };
462
- if (lsGet('vk_mines_installed') === '1') {
463
- var waited = 0;
464
- var poll = setInterval(function () {
465
- if (window.VK_HX_ADD) { clearInterval(poll); try { window.VK_HX_ADD(window.VK_MINES_APP); } catch (e) { /* ignore */ } }
466
- else if ((waited += 200) >= 10000) clearInterval(poll);
467
- }, 200);
468
- }
469
- })();
 
 
1
+ /* ViKernel Desktop: Minesweeper (VDR app). Original code and art, drawn with CSS/SVG. */
2
+ (function () {
3
+ 'use strict';
4
+
5
+ // ---------- i18n ----------
6
+ var STR = {
7
+ en: {
8
+ mines_title: 'Minesweeper', mines_beginner: 'Beginner', mines_intermediate: 'Intermediate',
9
+ mines_best: 'Best', mines_none: 'none', mines_new: 'New game', mines_hint: 'AI hint (Muse Spark)',
10
+ mines_cheat: 'Try to cheat: read mine map', mines_agent: 'Agent and kernel', mines_audit: 'App audit',
11
+ mines_win: 'Cleared! Time', mines_lose: 'Boom. All mines revealed.', mines_newbest: 'New best time!',
12
+ mines_thinking: 'Muse Spark is reading the visible board...', mines_empty_agent: 'No agent activity yet. Ask for an AI hint to see the agent propose a tool call and the kernel decide.',
13
+ mines_empty_audit: 'No verdicts recorded in this session yet.', mines_suggest: 'AI suggestion, may be wrong',
14
+ mines_rate: 'Too many requests. Please wait a moment and try again.', mines_err: 'The request failed',
15
+ mines_nohint: 'The model did not name a hidden cell this time.', mines_over: 'Start a new game to ask for a hint.',
16
+ mines_about: 'About', mines_close: 'Close', mines_retry: 'Retry'
17
+ },
18
+ ko: {
19
+ mines_title: '지뢰찾기', mines_beginner: '초급', mines_intermediate: '중급', mines_best: '최고', mines_none: '없음',
20
+ mines_new: '새 게임', mines_hint: 'AI 힌트 (Muse Spark)', mines_cheat: '부정행위 시도: 지뢰 지도 읽기',
21
+ mines_agent: '에이전트와 커널', mines_audit: '앱 감사 기록', mines_win: '성공! 시간', mines_lose: '펑. 모든 지뢰를 공개합니다.',
22
+ mines_newbest: '최고 기록!', mines_thinking: 'Muse Spark 가 보이는 판을 읽는 중...',
23
+ mines_empty_agent: '아직 에이전트 활동이 없습니다. AI 힌트를 요청해 보세요.', mines_empty_audit: '이 세션의 판정 기록이 아직 없습니다.',
24
+ mines_suggest: 'AI 제안, 틀릴 수 있음', mines_rate: '요청이 너무 많습니다. 잠시 후 다시 시도하세요.', mines_err: '요청 실패',
25
+ mines_nohint: '이번에는 모델이 숨은 칸을 지목하지 않았습니다.', mines_over: '힌트를 받으려면 새 게임을 시작하세요.',
26
+ mines_about: '정보', mines_close: '닫기', mines_retry: '다시 시도'
27
+ }
28
+ };
29
+ try {
30
+ window.VK_I18N = window.VK_I18N || {};
31
+ ['en', 'ko'].forEach(function (l) { window.VK_I18N[l] = Object.assign(window.VK_I18N[l] || {}, STR[l]); });
32
+ } catch (e) { /* ignore */ }
33
+
34
+ var MODES = {
35
+ beginner: { rows: 9, cols: 9, mines: 10, key: 'mines_beginner' },
36
+ intermediate: { rows: 16, cols: 16, mines: 40, key: 'mines_intermediate' }
37
+ };
38
+
39
+ function esc(s) {
40
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
41
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
42
+ });
43
+ }
44
+ function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
45
+ function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) { /* ignore */ } }
46
+
47
+ // ---------- art (original) ----------
48
+ var SVG_MINE = '<svg viewBox="0 0 20 20" aria-hidden="true"><g stroke="currentColor" stroke-width="1.6" stroke-linecap="round">' +
49
+ '<path d="M10 2.5v3M10 14.5v3M2.5 10h3M14.5 10h3M4.8 4.8l2 2M13.2 13.2l2 2M15.2 4.8l-2 2M6.8 13.2l-2 2"/></g>' +
50
+ '<rect x="5.6" y="5.6" width="8.8" height="8.8" rx="3" fill="currentColor"/><circle cx="8.6" cy="8.6" r="1.3" fill="#fff" opacity=".75"/></svg>';
51
+ var SVG_FLAG = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M6 3v14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
52
+ '<path d="M6.8 3.6l8.4 3.2-8.4 3.4z" fill="var(--mn-flag)"/><path d="M3.5 17h6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>';
53
+ function face(state) {
54
+ var eyes = '<circle cx="9" cy="10.5" r="1.4"/><circle cx="15" cy="10.5" r="1.4"/>', mouth = '<path d="M8.5 15c2 2 5 2 7 0" fill="none"/>';
55
+ if (state === 'tense') mouth = '<circle cx="12" cy="15.6" r="1.6" fill="none"/>';
56
+ if (state === 'lost') { eyes = '<path d="M7.6 9.1l2.8 2.8M10.4 9.1l-2.8 2.8M13.6 9.1l2.8 2.8M16.4 9.1l-2.8 2.8" fill="none"/>'; mouth = '<path d="M8.5 16.5c2-1.8 5-1.8 7 0" fill="none"/>'; }
57
+ if (state === 'won') { eyes = '<path d="M6.5 9.5h11M7 9.5c0 2.2 3.4 2.2 3.4 0M13.6 9.5c0 2.2 3.4 2.2 3.4 0" fill="none"/>'; mouth = '<path d="M8 14.6c2.4 2.8 5.6 2.8 8 0" fill="none"/>'; }
58
+ return '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2.5" y="2.5" width="19" height="19" rx="7" fill="var(--mn-face)" stroke="currentColor" stroke-width="1.4"/>' +
59
+ '<g fill="currentColor" stroke="currentColor" stroke-width="1.4" stroke-linecap="round">' + eyes + mouth + '</g></svg>';
60
+ }
61
+
62
+ // ---------- styles ----------
63
+ var CSS = '' +
64
+ '.mn{--mn-bg:#f6f8f7;--mn-panel:#fff;--mn-ink:#16211b;--mn-sub:#5b6b62;--mn-line:#dfe6e2;--mn-hid:#cfe3d6;--mn-hid2:#b9d6c3;--mn-open:#fbfdfc;' +
65
+ '--mn-acc:#16A34A;--mn-flag:#e4572e;--mn-face:#ffd76a;--mn-boom:#fde2dc;--mn-ok:#15803d;--mn-bad:#dc2626;' +
66
+ '--n1:#2563eb;--n2:#0f8a5f;--n3:#d9480f;--n4:#7c3aed;--n5:#b42318;--n6:#0e7490;--n7:#334155;--n8:#a16207;' +
67
+ 'display:flex;flex-direction:column;height:100%;background:var(--mn-bg);color:var(--mn-ink);font:13px/1.4 system-ui,-apple-system,"Segoe UI",sans-serif;box-sizing:border-box;position:relative;overflow:hidden}' +
68
+ '@media (prefers-color-scheme:dark){.mn{--mn-bg:#101613;--mn-panel:#17201b;--mn-ink:#e6efe9;--mn-sub:#93a59a;--mn-line:#26332c;--mn-hid:#2a3d32;--mn-hid2:#335042;--mn-open:#141c18;' +
69
+ '--mn-face:#e8bf4c;--mn-boom:#4a1f1a;--n1:#7aa7ff;--n2:#4fd19c;--n3:#ff9b5e;--n4:#c4a1ff;--n5:#ff7a73;--n6:#5fd3e6;--n7:#cbd5e1;--n8:#f1c84b}}' +
70
+ '.mn *{box-sizing:border-box}.mn button{font:inherit;color:inherit}' +
71
+ '.mn-strip{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:6px 12px;background:color-mix(in srgb,var(--mn-acc) 12%,var(--mn-panel));border-bottom:1px solid var(--mn-line);font-size:11.5px;color:var(--mn-sub)}' +
72
+ '.mn-strip b{color:var(--mn-acc)}.mn-strip .mn-i{margin-left:auto}' +
73
+ '.mn-i{width:24px;height:24px;border-radius:50%;border:1px solid var(--mn-line);background:var(--mn-panel);cursor:pointer;font-weight:700;font-style:italic;font-family:Georgia,serif}' +
74
+ '.mn-main{flex:1;display:flex;gap:12px;padding:12px;min-height:0}' +
75
+ '.mn-left{flex:1 1 auto;display:flex;flex-direction:column;gap:10px;min-width:0;min-height:0}' +
76
+ '.mn-side{flex:0 0 230px;display:flex;flex-direction:column;gap:10px;min-height:0;overflow:auto}' +
77
+ '.mn.narrow .mn-main{flex-direction:column;overflow:auto}.mn.narrow .mn-side{flex:0 0 auto;overflow:visible}.mn.narrow .mn-left{flex:0 0 auto}' +
78
+ '.mn-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}' +
79
+ '.mn-seg{display:inline-flex;border:1px solid var(--mn-line);border-radius:9px;overflow:hidden;background:var(--mn-panel)}' +
80
+ '.mn-seg button{border:0;background:none;padding:5px 10px;cursor:pointer}.mn-seg button[aria-pressed=true]{background:var(--mn-acc);color:#fff}' +
81
+ '.mn-lcd{font:600 15px ui-monospace,Consolas,monospace;padding:4px 9px;border-radius:8px;background:var(--mn-panel);border:1px solid var(--mn-line);min-width:52px;text-align:center}' +
82
+ '.mn-face{width:38px;height:38px;border-radius:11px;border:1px solid var(--mn-line);background:var(--mn-panel);cursor:pointer;padding:3px}.mn-face svg{width:100%;height:100%}' +
83
+ '.mn-face:active{transform:scale(.95)}.mn-best{color:var(--mn-sub);font-size:12px}.mn-sp{flex:1}' +
84
+ '.mn-wrap{flex:1;min-height:180px;display:flex;align-items:center;justify-content:center;position:relative}' +
85
+ '.mn-grid{display:grid;gap:2px;padding:6px;border-radius:12px;background:var(--mn-line);outline:none;touch-action:manipulation;user-select:none;-webkit-user-select:none}' +
86
+ '.mn-grid:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--mn-acc) 45%,transparent)}' +
87
+ '.mn-c{width:var(--cs);height:var(--cs);border:0;border-radius:calc(var(--cs)*.18);padding:0;display:flex;align-items:center;justify-content:center;cursor:pointer;' +
88
+ 'font:800 calc(var(--cs)*.55)/1 ui-rounded,system-ui,sans-serif;background:linear-gradient(160deg,var(--mn-hid),var(--mn-hid2));position:relative;-webkit-touch-callout:none}' +
89
+ '.mn-c svg{width:68%;height:68%}.mn-c.o{background:var(--mn-open);cursor:default}.mn-c.boom{background:var(--mn-boom)}.mn-c.bad::after{content:"";position:absolute;inset:18%;' +
90
+ 'background:linear-gradient(45deg,transparent 45%,var(--mn-bad) 45% 55%,transparent 55%)}' +
91
+ '.mn-c.foc{outline:2px solid var(--mn-acc);outline-offset:-2px;z-index:1}' +
92
+ '.mn-c.hint::before{content:"";position:absolute;inset:-3px;border-radius:inherit;border:3px solid #f59e0b;animation:mnp 1.1s ease-in-out infinite}' +
93
+ '@keyframes mnp{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.18);opacity:.35}}' +
94
+ '.mn-c[data-n="1"]{color:var(--n1)}.mn-c[data-n="2"]{color:var(--n2)}.mn-c[data-n="3"]{color:var(--n3)}.mn-c[data-n="4"]{color:var(--n4)}' +
95
+ '.mn-c[data-n="5"]{color:var(--n5)}.mn-c[data-n="6"]{color:var(--n6)}.mn-c[data-n="7"]{color:var(--n7)}.mn-c[data-n="8"]{color:var(--n8)}' +
96
+ '.mn-status{min-height:20px;text-align:center;font-weight:600}.mn-status.win{color:var(--mn-ok)}.mn-status.lose{color:var(--mn-bad)}' +
97
+ '.mn-hintlbl{font-size:12px;color:#b45309;text-align:center;min-height:16px}' +
98
+ '.mn-actions{display:flex;flex-direction:column;gap:6px}' +
99
+ '.mn-btn{display:flex;align-items:center;justify-content:center;gap:7px;padding:8px 10px;border-radius:9px;border:1px solid var(--mn-line);background:var(--mn-panel);cursor:pointer;font-weight:600}' +
100
+ '.mn-btn.pri{background:var(--mn-acc);border-color:var(--mn-acc);color:#fff}.mn-btn.warn{border-color:color-mix(in srgb,var(--mn-bad) 45%,var(--mn-line));color:var(--mn-bad)}' +
101
+ '.mn-btn:disabled{opacity:.6;cursor:wait}.mn-spin{width:13px;height:13px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:mns .7s linear infinite}' +
102
+ '@keyframes mns{to{transform:rotate(360deg)}}' +
103
+ '.mn-card{background:var(--mn-panel);border:1px solid var(--mn-line);border-radius:11px;padding:10px}' +
104
+ '.mn-card h4{margin:0 0 7px;font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:var(--mn-sub)}' +
105
+ '.mn-ev{border-left:3px solid var(--mn-line);padding:4px 0 4px 8px;margin:6px 0;word-break:break-word}.mn-ev code{font-size:11.5px}' +
106
+ '.mn-ev small{display:block;color:var(--mn-sub)}.mn-empty{color:var(--mn-sub);font-size:12px}.mn-errbox{color:var(--mn-bad);font-size:12px}' +
107
+ '.mn-tag{display:inline-block;padding:1px 7px;border-radius:6px;font-size:11px;font-weight:800;color:#fff;margin-right:5px}' +
108
+ '.mn-tag.allow{background:var(--mn-ok)}.mn-tag.deny{background:var(--mn-bad)}' +
109
+ '.mn-audit{list-style:none;margin:0;padding:0;font-size:12px}.mn-audit li{display:flex;gap:6px;align-items:center;padding:3px 0;border-bottom:1px dashed var(--mn-line)}' +
110
+ '.mn-audit li:last-child{border:0}.mn-audit span.k{color:var(--mn-sub);margin-left:auto;font-family:ui-monospace,monospace}' +
111
+ '.mn-deny{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(420px,92%);z-index:20;background:var(--mn-panel);border:2px solid var(--mn-bad);' +
112
+ 'border-radius:16px;padding:18px;box-shadow:0 18px 50px rgba(0,0,0,.3);animation:mnin .18s ease-out}' +
113
+ '@keyframes mnin{from{transform:translate(-50%,-46%);opacity:0}}' +
114
+ '.mn-deny .big{font-size:26px;font-weight:900;color:var(--mn-bad);letter-spacing:.06em}.mn-deny p{margin:8px 0}.mn-deny code{word-break:break-all}' +
115
+ '.mn-over{position:absolute;inset:0;background:rgba(0,0,0,.35);z-index:19}' +
116
+ '.mn-about{position:absolute;inset:24px;z-index:20;background:var(--mn-panel);border:1px solid var(--mn-line);border-radius:14px;padding:18px;overflow:auto;box-shadow:0 18px 50px rgba(0,0,0,.3)}' +
117
+ '.mn-about h3{margin:0 0 8px}.mn-about ul{padding-left:18px}';
118
+
119
+ function injectCSS() {
120
+ if (document.getElementById('vk-mines-css')) return;
121
+ var s = document.createElement('style'); s.id = 'vk-mines-css'; s.textContent = CSS; document.head.appendChild(s);
122
+ }
123
+
124
+ var ICON = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">' +
125
+ '<rect x="7" y="7" width="10" height="10" rx="3.5"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2 2M16.7 16.7l2 2M18.7 5.3l-2 2M7.3 16.7l-2 2"/></svg>';
126
+
127
+ // session-wide audit (persists across window reopen in this page load)
128
+ var AUDIT = [];
129
+
130
+ window.VK_MINES_APP = function (h) {
131
+ h = h || {};
132
+ function T(k) { try { var r = h.t && h.t(k); if (r && r !== k) return r; } catch (e) { /* ignore */ } return STR.en[k] || k; }
133
+ function toast(m) { try { if (h.toast) h.toast(m); } catch (e) { /* ignore */ } }
134
+
135
+ return {
136
+ id: 'mines', name: function () { return 'Minesweeper'; }, color: '#16A34A', icon: ICON, w: 560, h: 680,
137
+ render: function (body) {
138
+ injectCSS();
139
+ body.innerHTML = '';
140
+ var root = document.createElement('div'); root.className = 'mn';
141
+ root.innerHTML =
142
+ '<div class="mn-strip"><b>VDR app</b><span>·</span><span>Verified by ViKernel</span><span>·</span><span>declared scope: read game/visible/ only</span><span>·</span><span>no network</span>' +
143
+ '<button class="mn-i" data-a="about" title="' + esc(T('mines_about')) + '" aria-label="' + esc(T('mines_about')) + '">i</button></div>' +
144
+ '<div class="mn-main"><div class="mn-left">' +
145
+ '<div class="mn-head"><div class="mn-seg" role="group">' +
146
+ '<button data-mode="beginner">' + esc(T('mines_beginner')) + '</button><button data-mode="intermediate">' + esc(T('mines_intermediate')) + '</button></div>' +
147
+ '<span class="mn-best"></span></div>' +
148
+ '<div class="mn-head"><span class="mn-lcd" data-r="count" title="Mines left">0</span><span class="mn-sp"></span>' +
149
+ '<button class="mn-face" data-a="new" title="' + esc(T('mines_new')) + ' (N)" aria-label="' + esc(T('mines_new')) + '"></button>' +
150
+ '<span class="mn-sp"></span><span class="mn-lcd" data-r="time" title="Time">0</span></div>' +
151
+ '<div class="mn-wrap"><div class="mn-grid" role="grid" tabindex="0" aria-label="Minefield"></div></div>' +
152
+ '<div class="mn-status" aria-live="polite"></div><div class="mn-hintlbl"></div></div>' +
153
+ '<div class="mn-side"><div class="mn-actions">' +
154
+ '<button class="mn-btn pri" data-a="hint">' + esc(T('mines_hint')) + '</button>' +
155
+ '<button class="mn-btn warn" data-a="cheat">' + esc(T('mines_cheat')) + '</button></div>' +
156
+ '<div class="mn-card"><h4>' + esc(T('mines_agent')) + '</h4><div data-r="agent"></div></div>' +
157
+ '<div class="mn-card"><h4>' + esc(T('mines_audit')) + '</h4><ul class="mn-audit" data-r="audit"></ul></div>' +
158
+ '</div></div>';
159
+ body.appendChild(root);
160
+
161
+ var q = function (s) { return root.querySelector(s); };
162
+ var gridEl = q('.mn-grid'), wrap = q('.mn-wrap'), faceBtn = q('.mn-face'), countEl = q('[data-r=count]'), timeEl = q('[data-r=time]');
163
+ var statusEl = q('.mn-status'), hintLbl = q('.mn-hintlbl'), bestEl = q('.mn-best'), agentEl = q('[data-r=agent]'), auditEl = q('[data-r=audit]');
164
+ var hintBtn = q('[data-a=hint]'), cheatBtn = q('[data-a=cheat]');
165
+
166
+ var mode = lsGet('vk_mines_mode') === 'intermediate' ? 'intermediate' : 'beginner';
167
+ var R, C, M, mine, open, flag, adj, cells, started, over, won, t0, elapsed, timer, focus = 0, hintIdx = -1, opened;
168
+
169
+ // ---------- game core ----------
170
+ function nbrs(i) {
171
+ var r = (i / C) | 0, c = i % C, out = [];
172
+ for (var dr = -1; dr <= 1; dr++) for (var dc = -1; dc <= 1; dc++) {
173
+ if (!dr && !dc) continue; var rr = r + dr, cc = c + dc;
174
+ if (rr >= 0 && rr < R && cc >= 0 && cc < C) out.push(rr * C + cc);
175
+ }
176
+ return out;
177
+ }
178
+ function newGame(m) {
179
+ if (m) { mode = m; lsSet('vk_mines_mode', m); }
180
+ var cfg = MODES[mode]; R = cfg.rows; C = cfg.cols; M = cfg.mines;
181
+ var n = R * C; mine = new Array(n).fill(false); open = new Array(n).fill(false); flag = new Array(n).fill(false); adj = new Array(n).fill(0);
182
+ started = false; over = false; won = false; elapsed = 0; opened = 0; hintIdx = -1; focus = Math.min(focus, n - 1);
183
+ stopTimer(); statusEl.textContent = ''; statusEl.className = 'mn-status'; hintLbl.textContent = '';
184
+ root.querySelectorAll('.mn-seg button').forEach(function (b) { b.setAttribute('aria-pressed', String(b.dataset.mode === mode)); });
185
+ buildGrid(); updateHud(); fit();
186
+ }
187
+ function plant(safe) {
188
+ var ban = {}; ban[safe] = 1; nbrs(safe).forEach(function (j) { ban[j] = 1; });
189
+ var pool = []; for (var i = 0; i < R * C; i++) if (!ban[i]) pool.push(i);
190
+ for (var k = 0; k < M; k++) { var j = k + Math.floor(Math.random() * (pool.length - k)); var tmp = pool[k]; pool[k] = pool[j]; pool[j] = tmp; mine[pool[k]] = true; }
191
+ for (var x = 0; x < R * C; x++) adj[x] = nbrs(x).filter(function (y) { return mine[y]; }).length;
192
+ }
193
+ function reveal(i) {
194
+ if (over || open[i] || flag[i]) return;
195
+ if (!started) { plant(i); started = true; t0 = Date.now(); timer = setInterval(tick, 250); }
196
+ if (mine[i]) { lose(i); return; }
197
+ var stack = [i];
198
+ while (stack.length) {
199
+ var k = stack.pop(); if (open[k] || flag[k]) continue;
200
+ open[k] = true; opened++; paint(k);
201
+ if (adj[k] === 0) nbrs(k).forEach(function (j) { if (!open[j] && !mine[j]) stack.push(j); });
202
+ }
203
+ clearHint(); checkWin(); updateHud();
204
+ }
205
+ function chord(i) {
206
+ if (over || !open[i] || !adj[i]) return;
207
+ var ns = nbrs(i), f = ns.filter(function (j) { return flag[j]; }).length;
208
+ if (f !== adj[i]) return;
209
+ ns.forEach(function (j) { if (!open[j] && !flag[j]) reveal(j); });
210
+ }
211
+ function toggleFlag(i) {
212
+ if (over || open[i]) return;
213
+ flag[i] = !flag[i]; paint(i); updateHud();
214
+ }
215
+ function act(i) { if (open[i]) chord(i); else reveal(i); }
216
+ function lose(i) {
217
+ over = true; stopTimer();
218
+ for (var k = 0; k < R * C; k++) {
219
+ if (mine[k] && !flag[k]) { open[k] = true; paint(k); }
220
+ else if (flag[k] && !mine[k]) cells[k].classList.add('bad');
221
+ }
222
+ cells[i].classList.add('boom'); clearHint();
223
+ statusEl.textContent = T('mines_lose'); statusEl.className = 'mn-status lose'; updateHud();
224
+ }
225
+ function checkWin() {
226
+ if (over || opened !== R * C - M) return;
227
+ over = true; won = true; stopTimer();
228
+ for (var k = 0; k < R * C; k++) if (mine[k] && !flag[k]) { flag[k] = true; paint(k); }
229
+ var secs = Math.round(elapsed / 10) / 100, key = 'vk_mines_best_' + mode, prev = parseFloat(lsGet(key));
230
+ var msg = T('mines_win') + ' ' + secs.toFixed(1) + 's';
231
+ if (!(prev > 0) || secs < prev) { lsSet(key, String(secs)); msg += '. ' + T('mines_newbest'); }
232
+ statusEl.textContent = msg; statusEl.className = 'mn-status win'; toast(msg);
233
+ }
234
+ function tick() {
235
+ if (!root.isConnected) { stopTimer(); return; }
236
+ elapsed = Date.now() - t0; timeEl.textContent = String(Math.min(999, Math.floor(elapsed / 1000)));
237
+ }
238
+ function stopTimer() { if (timer) { clearInterval(timer); timer = null; if (started) elapsed = Date.now() - t0; } }
239
+ function updateHud() {
240
+ var f = flag.filter(Boolean).length; countEl.textContent = String(M - f);
241
+ if (!started) timeEl.textContent = '0';
242
+ faceBtn.innerHTML = face(over ? (won ? 'won' : 'lost') : 'ok');
243
+ var b = parseFloat(lsGet('vk_mines_best_' + mode));
244
+ bestEl.textContent = T('mines_best') + ' (' + T(MODES[mode].key) + '): ' + (b > 0 ? b.toFixed(1) + 's' : T('mines_none'));
245
+ }
246
+
247
+ // ---------- rendering ----------
248
+ function buildGrid() {
249
+ gridEl.innerHTML = ''; gridEl.style.gridTemplateColumns = 'repeat(' + C + ',var(--cs))'; cells = [];
250
+ var frag = document.createDocumentFragment();
251
+ for (var i = 0; i < R * C; i++) {
252
+ var b = document.createElement('div'); b.className = 'mn-c'; b.dataset.i = i; b.setAttribute('role', 'gridcell');
253
+ frag.appendChild(b); cells.push(b); paint(i);
254
+ }
255
+ gridEl.appendChild(frag); setFocus(focus, false);
256
+ }
257
+ function paint(i) {
258
+ var el = cells[i]; if (!el) return;
259
+ var r = (i / C) | 0, c = i % C, label = 'row ' + r + ' col ' + c + ': ';
260
+ el.classList.toggle('o', open[i]); el.removeAttribute('data-n');
261
+ if (open[i] && mine[i]) { el.innerHTML = SVG_MINE; label += 'mine'; }
262
+ else if (open[i]) { el.innerHTML = adj[i] ? String(adj[i]) : ''; if (adj[i]) el.dataset.n = adj[i]; label += adj[i] || 'empty'; }
263
+ else if (flag[i]) { el.innerHTML = SVG_FLAG; label += 'flagged'; }
264
+ else { el.innerHTML = ''; label += 'hidden'; }
265
+ el.setAttribute('aria-label', label);
266
+ }
267
+ function setFocus(i, scroll) {
268
+ if (cells[focus]) cells[focus].classList.remove('foc');
269
+ focus = Math.max(0, Math.min(R * C - 1, i));
270
+ if (cells[focus]) { cells[focus].classList.add('foc'); gridEl.setAttribute('aria-activedescendant', ''); if (scroll) cells[focus].scrollIntoView({ block: 'nearest' }); }
271
+ }
272
+ function fit() {
273
+ var narrow = root.clientWidth < 600; root.classList.toggle('narrow', narrow);
274
+ var w = wrap.clientWidth - 14, hh = narrow ? Math.min(window.innerHeight * 0.6, w) : wrap.clientHeight - 14;
275
+ var cs = Math.floor(Math.min((w - (C - 1) * 2) / C, (hh - (R - 1) * 2) / R));
276
+ cs = Math.max(narrow ? 20 : 16, Math.min(46, cs || 24));
277
+ gridEl.style.setProperty('--cs', cs + 'px');
278
+ }
279
+ var ro = window.ResizeObserver ? new ResizeObserver(fit) : null;
280
+ if (ro) ro.observe(root); else window.addEventListener('resize', fit);
281
+
282
+ // ---------- input ----------
283
+ var lpTimer = null, lpFired = false, lastTouch = 0;
284
+ function idxOf(e) { var t = e.target.closest && e.target.closest('.mn-c'); return t ? +t.dataset.i : -1; }
285
+ gridEl.addEventListener('pointerdown', function (e) {
286
+ var i = idxOf(e); if (i < 0) return;
287
+ lpFired = false; if (!over && !open[i]) faceBtn.innerHTML = face('tense');
288
+ if (e.pointerType === 'touch') {
289
+ lastTouch = Date.now(); clearTimeout(lpTimer);
290
+ lpTimer = setTimeout(function () { lpFired = true; toggleFlag(i); if (navigator.vibrate) navigator.vibrate(15); }, 400);
291
+ }
292
+ });
293
+ function cancelLp() { clearTimeout(lpTimer); updateHud(); }
294
+ gridEl.addEventListener('pointerup', cancelLp);
295
+ gridEl.addEventListener('pointercancel', cancelLp);
296
+ gridEl.addEventListener('pointerleave', cancelLp);
297
+ gridEl.addEventListener('click', function (e) {
298
+ var i = idxOf(e); if (i < 0) return;
299
+ if (lpFired) { lpFired = false; return; }
300
+ setFocus(i, false);
301
+ if (e.shiftKey) toggleFlag(i); else act(i);
302
+ });
303
+ gridEl.addEventListener('contextmenu', function (e) {
304
+ e.preventDefault(); var i = idxOf(e); if (i < 0) return;
305
+ if (Date.now() - lastTouch < 800) return; // touch handled by long-press
306
+ setFocus(i, false); toggleFlag(i);
307
+ });
308
+ gridEl.addEventListener('keydown', function (e) {
309
+ var k = e.key, r = (focus / C) | 0, c = focus % C;
310
+ if (k === 'ArrowUp') setFocus(Math.max(0, r - 1) * C + c, true);
311
+ else if (k === 'ArrowDown') setFocus(Math.min(R - 1, r + 1) * C + c, true);
312
+ else if (k === 'ArrowLeft') setFocus(r * C + Math.max(0, c - 1), true);
313
+ else if (k === 'ArrowRight') setFocus(r * C + Math.min(C - 1, c + 1), true);
314
+ else if (k === ' ' || k === 'Enter') act(focus);
315
+ else if (k === 'f' || k === 'F') toggleFlag(focus);
316
+ else if (k === 'n' || k === 'N') newGame();
317
+ else return;
318
+ e.preventDefault();
319
+ });
320
+ root.querySelectorAll('.mn-seg button').forEach(function (b) {
321
+ b.addEventListener('click', function () { newGame(b.dataset.mode); gridEl.focus(); });
322
+ });
323
+ faceBtn.addEventListener('click', function () { newGame(); gridEl.focus(); });
324
+
325
+ // ---------- ViKernel features ----------
326
+ function boardText() {
327
+ var lines = ['rows ' + R + ' cols ' + C];
328
+ for (var r = 0; r < R; r++) {
329
+ var s = '';
330
+ for (var c = 0; c < C; c++) { var i = r * C + c; s += open[i] ? (mine[i] ? '#' : String(adj[i])) : (flag[i] ? 'F' : '#'); }
331
+ lines.push(s);
332
+ }
333
+ return lines.join('\n'); // visible state only: mine positions are never sent
334
+ }
335
+ function clearHint() {
336
+ if (hintIdx >= 0 && cells[hintIdx]) cells[hintIdx].classList.remove('hint');
337
+ hintIdx = -1; hintLbl.textContent = '';
338
+ }
339
+ function tag(v) { var a = String(v || '').toUpperCase() === 'ALLOW'; return '<span class="mn-tag ' + (a ? 'allow' : 'deny') + '">' + (a ? 'ALLOW' : 'DENY') + '</span>'; }
340
+ function renderAudit() {
341
+ if (!AUDIT.length) { auditEl.innerHTML = '<li class="mn-empty">' + esc(T('mines_empty_audit')) + '</li>'; return; }
342
+ auditEl.innerHTML = AUDIT.slice().reverse().map(function (a) {
343
+ return '<li>' + tag(a.verdict) + '<span>' + esc(a.kind) + '</span><span class="k">' + (a.seq != null ? 'audit #' + esc(a.seq) : esc(a.time)) + '</span></li>';
344
+ }).join('');
345
+ }
346
+ function addAudit(kind, verdict, seq) {
347
+ AUDIT.push({ kind: kind, verdict: verdict, seq: seq, time: new Date().toLocaleTimeString() }); renderAudit();
348
+ }
349
+ function agentEmpty() { agentEl.innerHTML = '<div class="mn-empty">' + esc(T('mines_empty_agent')) + '</div>'; }
350
+ function errMsg(e) { return e && e.message === 'rate' ? T('mines_rate') : T('mines_err') + ': ' + (e && e.message || 'unknown'); }
351
+ function setBusy(btn, on, label) {
352
+ btn.disabled = on; btn.innerHTML = on ? '<span class="mn-spin"></span>' + esc(label) : esc(label);
353
+ }
354
+
355
+ function renderEvents(evs) {
356
+ var html = '';
357
+ evs.forEach(function (ev) {
358
+ if (ev.type === 'proposed') {
359
+ html += '<div class="mn-ev"><b>Proposed</b> <code>' + esc(ev.fn) + '(' + esc(ev.args && ev.args.path ? ev.args.path : JSON.stringify(ev.args || {})) + ')</code>' +
360
+ '<small>' + esc(ev.model || 'model') + (ev.model_ms != null ? ' · ' + esc(ev.model_ms) + ' ms' : '') + '</small></div>';
361
+ } else if (ev.type === 'verdict') {
362
+ html += '<div class="mn-ev">' + tag(ev.verdict) + (ev.audit_seq != null ? '<b>audit #' + esc(ev.audit_seq) + '</b>' : '') +
363
+ '<small>rule ' + esc(ev.rule || '-') + (ev.reason ? ' · ' + esc(ev.reason) : '') + '</small></div>';
364
+ } else if (ev.type === 'final') {
365
+ html += '<div class="mn-ev"><b>Final</b><div>' + esc(ev.text) + '</div></div>';
366
+ }
367
+ });
368
+ agentEl.innerHTML = html || '<div class="mn-empty">' + esc(T('mines_nohint')) + '</div>';
369
+ }
370
+
371
+ hintBtn.addEventListener('click', async function () {
372
+ if (over) { toast(T('mines_over')); hintLbl.textContent = T('mines_over'); return; }
373
+ clearHint(); setBusy(hintBtn, true, T('mines_hint'));
374
+ agentEl.innerHTML = '<div class="mn-empty"><span class="mn-spin" style="display:inline-block;vertical-align:-2px;margin-right:6px"></span>' + esc(T('mines_thinking')) + '</div>';
375
+ try {
376
+ var res = await window.VK_API('game/hint', { session: window.VK_SID, board: boardText() });
377
+ var evs = (res && res.events) || [];
378
+ renderEvents(evs);
379
+ var final = '';
380
+ evs.forEach(function (ev) {
381
+ if (ev.type === 'verdict') addAudit('hint: ' + (ev.fn || 'read_file'), ev.verdict, ev.audit_seq);
382
+ if (ev.type === 'final') final = ev.text || '';
383
+ });
384
+ var m = /(\d+)\s*,\s*(\d+)/.exec(final);
385
+ if (m) {
386
+ var r = +m[1], c = +m[2], i = r * C + c;
387
+ if (r < R && c < C && !open[i] && !flag[i] && !over) {
388
+ hintIdx = i; cells[i].classList.add('hint'); setFocus(i, true);
389
+ hintLbl.textContent = T('mines_suggest') + ' (row ' + r + ', col ' + c + ')'; return;
390
+ }
391
+ }
392
+ hintLbl.textContent = T('mines_nohint');
393
+ } catch (e) {
394
+ agentEl.innerHTML = '<div class="mn-errbox">' + esc(errMsg(e)) + '</div><button class="mn-btn" data-a="retry" style="margin-top:6px;width:100%">' + esc(T('mines_retry')) + '</button>';
395
+ var rb = agentEl.querySelector('[data-a=retry]'); rb.addEventListener('click', function () { hintBtn.click(); });
396
+ } finally { setBusy(hintBtn, false, T('mines_hint')); }
397
+ });
398
+
399
+ var overlay = null;
400
+ function closeOverlay() { if (overlay) { overlay.forEach(function (n) { n.remove(); }); overlay = null; gridEl.focus(); } }
401
+ function showOverlay(cls, html) {
402
+ closeOverlay();
403
+ var bg = document.createElement('div'); bg.className = 'mn-over'; bg.addEventListener('click', closeOverlay);
404
+ var box = document.createElement('div'); box.className = cls; box.setAttribute('role', 'dialog'); box.innerHTML = html;
405
+ root.appendChild(bg); root.appendChild(box); overlay = [bg, box];
406
+ var cb = box.querySelector('[data-a=close]'); if (cb) { cb.addEventListener('click', closeOverlay); cb.focus(); }
407
+ }
408
+ root.addEventListener('keydown', function (e) { if (e.key === 'Escape' && overlay) { closeOverlay(); e.stopPropagation(); } });
409
+
410
+ cheatBtn.addEventListener('click', async function () {
411
+ setBusy(cheatBtn, true, T('mines_cheat'));
412
+ var ctl = null, done = false;
413
+ try {
414
+ var res = await Promise.race([
415
+ window.VK_API('game/cheat', {}),
416
+ new Promise(function (_, rej) { ctl = setTimeout(function () { if (!done) rej(new Error('timeout')); }, 8000); })
417
+ ]);
418
+ done = true; clearTimeout(ctl);
419
+ var path = (res && res.args && res.args.path) || 'game/secret/mines.map';
420
+ var v = String(res && res.verdict || 'DENY').toUpperCase();
421
+ addAudit('cheat: ' + (res && res.fn || 'read_file'), v, res && res.audit_seq);
422
+ showOverlay('mn-deny',
423
+ '<div class="big">' + (v === 'ALLOW' ? 'ALLOW' : 'DENY') + '</div>' +
424
+ '<p><b>DENY · rule ' + esc(res && res.rule || 'manifest') + ' · path ' + esc(path) + ' is outside the declared scope (game/visible/)</b></p>' +
425
+ '<p>Call: <code>' + esc(res && res.fn || 'read_file') + '("' + esc(path) + '")</code>' + (res && res.app ? ' by <code>' + esc(res.app) + '</code>' : '') + '</p>' +
426
+ (res && res.reason ? '<p>Reason (manifest rule): ' + esc(res.reason) + '</p>' : '') +
427
+ (res && res.audit_seq != null ? '<p>Recorded as audit #' + esc(res.audit_seq) + '.</p>' : '') +
428
+ '<p style="color:var(--mn-sub)">The app never saw the mine map. Your game is unchanged.</p>' +
429
+ '<button class="mn-btn" data-a="close" style="width:100%">' + esc(T('mines_close')) + '</button>');
430
+ } catch (e) {
431
+ done = true; clearTimeout(ctl);
432
+ showOverlay('mn-deny', '<div class="big" style="font-size:18px">' + esc(T('mines_err')) + '</div><p>' + esc(errMsg(e)) + '</p>' +
433
+ '<button class="mn-btn" data-a="close" style="width:100%">' + esc(T('mines_close')) + '</button>');
434
+ } finally { setBusy(cheatBtn, false, T('mines_cheat')); }
435
+ });
436
+
437
+ q('[data-a=about]').addEventListener('click', function () {
438
+ showOverlay('mn-about',
439
+ '<h3>Minesweeper for ViKernel Desktop</h3>' +
440
+ '<p>Clear every safe cell without opening a mine. Numbers show how many mines touch a cell. The first click is always safe.</p>' +
441
+ '<ul><li>Click or tap: reveal. Click a number with matching flags around it to open its neighbors.</li>' +
442
+ '<li>Right-click, Shift+click or long-press: flag.</li><li>Keys: arrows move, Space or Enter reveals, F flags, N starts a new game.</li></ul>' +
443
+ '<h3>Declared scope</h3>' +
444
+ '<p>This is a VDR app verified by ViKernel. Its manifest declares one permission: read files under <code>game/visible/</code>. It makes no network calls of its own. ' +
445
+ 'The AI hint sends only the visible board (hidden cells as #, flags as F, revealed numbers). Mine positions stay inside this window and are never shared.</p>' +
446
+ '<p>"Try to cheat" asks the kernel to read <code>game/secret/mines.map</code>. The path is outside the declared scope, so the kernel denies it and logs the attempt in the audit list.</p>' +
447
+ '<p>AI hints come from Muse Spark and may be wrong. Hints are highlighted, never played for you.</p>' +
448
+ '<button class="mn-btn" data-a="close">' + esc(T('mines_close')) + '</button>');
449
+ });
450
+
451
+ agentEmpty(); renderAudit(); newGame();
452
+ setTimeout(fit, 0);
453
+ }
454
+ };
455
+ };
456
+
457
+ // ---------- install ----------
458
+ window.VK_MINES_INSTALL = function () {
459
+ lsSet('vk_mines_installed', '1');
460
+ if (window.VK_HX_ADD) window.VK_HX_ADD(window.VK_MINES_APP);
461
+ };
462
+ // preinstalled for every visitor (shipped from the App Store as a VDR package)
463
+ if (true) {
464
+ var waited = 0;
465
+ var poll = setInterval(function () {
466
+ if (window.VK_HX_ADD) { clearInterval(poll); try { window.VK_HX_ADD(window.VK_MINES_APP); } catch (e) { /* ignore */ } }
467
+ else if ((waited += 200) >= 10000) clearInterval(poll);
468
+ }, 200);
469
+ }
470
+ })();
store.js CHANGED
@@ -474,8 +474,9 @@
474
  }
475
  var a = D.data, p = a.permissions || {};
476
  var instBox = el('div', {});
477
- var ib = el('button', { type: 'button', class: 'btn pri' }, t('vs.install'));
478
- ib.addEventListener('click', function () { doInstall(a, ib, instBox); });
 
479
  w.appendChild(el('div', { class: 'vs-panel' }, el('div', { class: 'vs-dhead' }, iconImg(a, ''),
480
  el('div', { style: 'flex:1;min-width:200px' }, el('h1', {}, a.name, (a.new || S.highlight === a.id) ? el('span', { class: 'vs-b', style: 'margin-left:8px;background:var(--vs-bad);color:#fff' }, 'NEW') : null),
481
  el('div', { class: 'vs-mu' }, (a.publisher || '') + ' · ' + (a.category || '') + ' · v' + (a.version || '?') + ' · ' + (a.installs || 0) + ' ' + t('vs.installs') + ' · ' + t('vs.new')),
 
474
  }
475
  var a = D.data, p = a.permissions || {};
476
  var instBox = el('div', {});
477
+ var preinst = a.id === 'vidraft.minesweeper';
478
+ var ib = el('button', { type: 'button', class: 'btn pri' }, preinst ? 'Installed · Open' : t('vs.install'));
479
+ ib.addEventListener('click', function () { if (preinst) { h.openApp('mines'); return; } doInstall(a, ib, instBox); });
480
  w.appendChild(el('div', { class: 'vs-panel' }, el('div', { class: 'vs-dhead' }, iconImg(a, ''),
481
  el('div', { style: 'flex:1;min-width:200px' }, el('h1', {}, a.name, (a.new || S.highlight === a.id) ? el('span', { class: 'vs-b', style: 'margin-left:8px;background:var(--vs-bad);color:#fff' }, 'NEW') : null),
482
  el('div', { class: 'vs-mu' }, (a.publisher || '') + ' · ' + (a.category || '') + ' · v' + (a.version || '?') + ' · ' + (a.installs || 0) + ' ' + t('vs.installs') + ' · ' + t('vs.new')),