diff --git a/AGENTS.md b/AGENTS.md
index ec3ddc2..6efb9e2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -20,8 +20,8 @@ sursa de adevăr tehnică pentru agenți.
python3 -m http.server 8000
# Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md):
-npx playwright test tests/smoke.mjs # suita completă: 26/26
-npx playwright test tests/smoke.mjs --grep @regresie # regresie: 14
+npx playwright test tests/smoke.mjs # suita completă: 27/27
+npx playwright test tests/smoke.mjs --grep @regresie # regresie: 15
npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 12
```
diff --git a/TODOS.md b/TODOS.md
index 2181694..3a02f53 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -20,6 +20,20 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2
**PR2 livrat (2026-06-13):** audio camere `651025b`, voce `da93d84`, unificare `ab11089`, a11y (acest commit).
Rămas din Etapa 2: D7 (migrare classic pe libJS+SNIP) + muzică timer (T10) + Adventure Mode v0.
+### [x] Bomberman polish (feedback user 2026-06-13) — LIVRAT
+Trei probleme raportate + o lipsă, toate în `gameArcade` (`escape-builder.html`):
+- **Fără sunete în joc** → adăugat `sfx(type)` (WebAudio local în iframe, deblocat de gesturile din
+ arcade): `bomb` (plasare), `explosion` (zgomot filtrat lowpass + thump sine), `enemy` (dușman ucis),
+ `powerup` (arpegiu), `death`. `beep(ok)` din libJS rămâne pt. răspuns corect/greșit.
+- **Rază prea mare** → `EXPLOSION_RANGE=3` const → `bombRange` variabil pornind de la `BASE_RANGE=1`
+ (Bomberman clasic). Similar `maxBombs` de la `BASE_BOMBS=1`.
+- **Fără powerup-uri** → la spargerea unei cutii, șansă `POWERUP_CHANCE=0.32` să cadă 🔥 (rază+1) sau
+ 💣 (bombe+1). Ridicate mergând pe ele; persistă peste respawn, reset la `init()`. HUD arată 💣/🔥.
+- **Bug prins** (drop=0 inițial): powerup-ul cădea pe celula cutiei, iar `checkExplosionHits` îl ștergea
+ instant ca fiind „pe o celulă de explozie". Fix: colectez `brokenBoxes`, dau drop DUPĂ `checkExplosionHits`.
+Teste noi: smoke #27 (rază 1 + drop supraviețuiește + pickup crește rază/bombe). Hooks `__game`:
+`powerups`/`bombRange`/`maxBombs`/`dropPowerupAt`. Verificat: smoke 27/27 + live (drop ~30%, 0 erori).
+
---
## ▶ BOARD ACTIV — Iterația 2 (Adventure Mode / restyle)
diff --git a/escape-builder.html b/escape-builder.html
index 0bfbf4a..6e0f14b 100644
--- a/escape-builder.html
+++ b/escape-builder.html
@@ -1046,7 +1046,7 @@ ${SNIP.baseCss}${SNIP.modalCss}${SNIP.finalCss}
Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.
+Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile (uneori cad bonusuri: 🔥 raza, 💣 bombe in plus), evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.
Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.
+Sageti / WASD = misca, Space sau 💣 = bomba. Sparge cutiile (uneori cad bonusuri: 🔥 raza, 💣 bombe in plus), evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.
@@ -111,12 +111,37 @@ window.__seed = __seed;
function makePRNG(seed){ var s = seed >>> 0; return function(){ s |= 0; s = (s + 0x6d2b79f5) | 0; var t = Math.imul(s ^ (s >>> 15), 1 | s); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
var rng = makePRNG(__seed);
+/* ----- Efecte sonore arcade (WebAudio local; deblocat de gesturile din iframe) -----
+ beep(ok) din libJS ramane pentru raspuns corect/gresit; sfx() adauga bomba/explozie/powerup. */
+function sfx(type){
+ try {
+ var actx = sfx.ctx || (sfx.ctx = new (window.AudioContext || window.webkitAudioContext)());
+ if (actx.state === 'suspended') actx.resume();
+ var t = actx.currentTime;
+ function tone(wave, f0, f1, dur, vol){ var o = actx.createOscillator(), g = actx.createGain(); o.type = wave; o.frequency.setValueAtTime(f0, t); if (f1 !== f0) o.frequency.exponentialRampToValueAtTime(f1, t + dur); g.gain.setValueAtTime(vol, t); g.gain.exponentialRampToValueAtTime(0.0008, t + dur); o.connect(g); g.connect(actx.destination); o.start(t); o.stop(t + dur + 0.02); }
+ if (type === 'bomb'){ tone('square', 440, 150, 0.1, 0.07); }
+ else if (type === 'explosion'){
+ var dur = 0.45, sr = actx.sampleRate, buf = actx.createBuffer(1, Math.floor(sr * dur), sr), data = buf.getChannelData(0);
+ for (var i = 0; i < data.length; i++){ var k = 1 - i / data.length; data[i] = (Math.random() * 2 - 1) * k * k; }
+ var src = actx.createBufferSource(); src.buffer = buf;
+ var lp = actx.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.setValueAtTime(1100, t); lp.frequency.exponentialRampToValueAtTime(180, t + dur);
+ var g = actx.createGain(); g.gain.setValueAtTime(0.38, t); g.gain.exponentialRampToValueAtTime(0.0008, t + dur);
+ src.connect(lp); lp.connect(g); g.connect(actx.destination); src.start(t);
+ tone('sine', 130, 42, 0.34, 0.3);
+ }
+ else if (type === 'enemy'){ tone('square', 200, 520, 0.14, 0.08); }
+ else if (type === 'powerup'){ var fs = [523, 659, 784, 1047]; for (var p = 0; p < fs.length; p++){ var o = actx.createOscillator(), gg = actx.createGain(); o.type = 'triangle'; o.frequency.value = fs[p]; gg.gain.setValueAtTime(0.08, t + p * 0.06); gg.gain.exponentialRampToValueAtTime(0.0008, t + p * 0.06 + 0.13); o.connect(gg); gg.connect(actx.destination); o.start(t + p * 0.06); o.stop(t + p * 0.06 + 0.15); } }
+ else if (type === 'death'){ tone('sawtooth', 330, 55, 0.5, 0.12); }
+ } catch (e) {}
+}
+
var GW = 15, GH = 13, TS = 36;
var T_FLOOR = 0, T_WALL = 1, T_BOX = 2, T_DOOR = 3, T_CHEST = 4;
-var BOMB_TIMER = 2400, EXPLOSION_TIME = 500, EXPLOSION_RANGE = 3, ENEMY_INTERVAL = 600, RESPAWN_DELAY = 1500, INVINCIBLE_TIME = 2000, MAX_LIVES = 3;
+var BOMB_TIMER = 2400, EXPLOSION_TIME = 500, BASE_RANGE = 1, BASE_BOMBS = 1, POWERUP_CHANCE = 0.32, ENEMY_INTERVAL = 600, RESPAWN_DELAY = 1500, INVINCIBLE_TIME = 2000, MAX_LIVES = 3;
var NUM_DOORS = N, NUM_ENEMIES = Math.max(2, Math.min(4, N + 1));
+var P_RANGE = 'range', P_BOMB = 'bomb';
-var map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos;
+var map, player, enemies, bombs, explosions, lives, gameOver, gameWon, puzzleProgress, doorMeta, chestPos, powerups, bombRange, maxBombs;
var animFrame, lastTime = 0, enemyTimer = 0, invincibleTimer = 0, bombIdCounter = 0;
var cv = el('cv'); cv.width = GW * TS; cv.height = GH * TS;
@@ -156,7 +181,7 @@ function init(){
shuffle(ec);
enemies = [];
for (var i = 0; i < NUM_ENEMIES && i < ec.length; i++) enemies.push({ x: ec[i].x, y: ec[i].y, alive: true, id: i });
- bombs = []; explosions = []; lives = MAX_LIVES; gameOver = false; gameWon = false; enemyTimer = 0; invincibleTimer = 0; lastTime = 0;
+ bombs = []; explosions = []; powerups = []; bombRange = BASE_RANGE; maxBombs = BASE_BOMBS; lives = MAX_LIVES; gameOver = false; gameWon = false; enemyTimer = 0; invincibleTimer = 0; lastTime = 0;
if (!puzzleProgress) puzzleProgress = { doorsSolved: [] };
for (var dd = 0; dd < doorMeta.length; dd++) if (puzzleProgress.doorsSolved[dd]) map[doorMeta[dd].y][doorMeta[dd].x] = T_FLOOR;
hideGameOver();
@@ -180,7 +205,7 @@ el('goRestart').onclick = function(){ init(); };
function updateHud(){
var solved = puzzleProgress ? puzzleProgress.doorsSolved.filter(Boolean).length : 0;
var alive = enemies ? enemies.filter(function(e){ return e.alive; }).length : 0;
- el('hudStep').textContent = '\u2764\ufe0f ' + lives + ' \ud83d\udc7e ' + alive + ' \ud83d\udd13 ' + solved + '/' + N;
+ el('hudStep').textContent = '\u2764\ufe0f ' + lives + ' \ud83d\udc7e ' + alive + ' \ud83d\udd13 ' + solved + '/' + N + ' \ud83d\udca3' + (maxBombs || 1) + ' \ud83d\udd25' + (bombRange || 1);
el('hudStars').textContent = totalStars + ' \u2605';
var hb = el('hudLetters'); hb.innerHTML = '';
for (var j = 0; j < N; j++){ var L = (CFG.puzzles[j].letter || '').trim(); if (!L) continue; var s = document.createElement('span'); if (puzzleProgress && puzzleProgress.doorsSolved[j]){ s.textContent = L.toUpperCase(); s.className = 'won'; } else s.textContent = '?'; hb.appendChild(s); }
@@ -189,27 +214,47 @@ function updateHud(){
/* ----- Bombe + explozii în lanț ----- */
function placeBomb(){
if (!player.alive || gameOver || gameWon || modalOpen()) return;
- if (bombs.length >= 1) return;
+ if (bombs.length >= maxBombs) return;
+ for (var i = 0; i < bombs.length; i++) if (bombs[i].x === player.x && bombs[i].y === player.y) return;
bombs.push({ x: player.x, y: player.y, timer: BOMB_TIMER, id: bombIdCounter++ });
+ sfx('bomb');
updateHud();
}
function explodeBomb(bomb){
bombs = bombs.filter(function(b){ return b.id !== bomb.id; });
var cells = [{ x: bomb.x, y: bomb.y }];
+ var brokenBoxes = [];
var dirs = [[1,0],[-1,0],[0,1],[0,-1]];
- for (var d = 0; d < dirs.length; d++){ var dx = dirs[d][0], dy = dirs[d][1]; for (var r = 1; r <= EXPLOSION_RANGE; r++){ var cx = bomb.x + dx * r, cy = bomb.y + dy * r; if (cx < 0 || cy < 0 || cx >= GW || cy >= GH) break; var t = map[cy][cx]; if (t === T_WALL) break; cells.push({ x: cx, y: cy }); if (t === T_BOX){ map[cy][cx] = T_FLOOR; break; } if (t === T_DOOR || t === T_CHEST) break; } }
+ for (var d = 0; d < dirs.length; d++){ var dx = dirs[d][0], dy = dirs[d][1]; for (var r = 1; r <= bombRange; r++){ var cx = bomb.x + dx * r, cy = bomb.y + dy * r; if (cx < 0 || cy < 0 || cx >= GW || cy >= GH) break; var t = map[cy][cx]; if (t === T_WALL) break; cells.push({ x: cx, y: cy }); if (t === T_BOX){ map[cy][cx] = T_FLOOR; brokenBoxes.push({ x: cx, y: cy }); break; } if (t === T_DOOR || t === T_CHEST) break; } }
explosions.push({ cells: cells, endTime: performance.now() + EXPLOSION_TIME });
+ sfx('explosion');
var chain = bombs.slice();
for (var i = 0; i < chain.length; i++){ var bb = chain[i]; for (var c = 0; c < cells.length; c++) if (cells[c].x === bb.x && cells[c].y === bb.y){ explodeBomb(bb); break; } }
checkExplosionHits(cells);
+ /* drop DUPA checkExplosionHits: altfel powerup-ul de pe celula cutiei e sters instant de filtrul de explozie */
+ for (var bx = 0; bx < brokenBoxes.length; bx++) maybeDropPowerup(brokenBoxes[bx].x, brokenBoxes[bx].y);
updateHud();
}
function checkExplosionHits(cells){
- for (var c = 0; c < cells.length; c++){ var cx = cells[c].x, cy = cells[c].y; for (var i = 0; i < enemies.length; i++) if (enemies[i].alive && enemies[i].x === cx && enemies[i].y === cy) enemies[i].alive = false; if (player.alive && !player.invincible && player.x === cx && player.y === cy) killPlayer(); }
+ for (var c = 0; c < cells.length; c++){ var cx = cells[c].x, cy = cells[c].y;
+ for (var i = 0; i < enemies.length; i++) if (enemies[i].alive && enemies[i].x === cx && enemies[i].y === cy){ enemies[i].alive = false; sfx('enemy'); }
+ powerups = powerups.filter(function(p){ return !(p.x === cx && p.y === cy); });
+ if (player.alive && !player.invincible && player.x === cx && player.y === cy) killPlayer();
+ }
+}
+function maybeDropPowerup(x, y){
+ if (rng() >= POWERUP_CHANCE) return;
+ powerups.push({ x: x, y: y, type: rng() < 0.5 ? P_RANGE : P_BOMB });
+}
+function pickupPowerup(){
+ for (var i = 0; i < powerups.length; i++) if (powerups[i].x === player.x && powerups[i].y === player.y){
+ if (powerups[i].type === P_RANGE) bombRange++; else maxBombs++;
+ powerups.splice(i, 1); sfx('powerup'); updateHud(); return;
+ }
}
function killPlayer(){
if (!player.alive) return;
- player.alive = false; lives--; updateHud();
+ player.alive = false; lives--; sfx('death'); updateHud();
setTimeout(function(){ if (lives > 0) respawn(); else showGameOver(); }, RESPAWN_DELAY);
}
@@ -226,6 +271,7 @@ function movePlayer(dir){
if (t === T_DOOR){ for (var d = 0; d < doorMeta.length; d++) if (doorMeta[d].x === nx && doorMeta[d].y === ny){ if (!puzzleProgress.doorsSolved[d]) openPuzzle(d, onDoorSolved); return; } return; }
if (t === T_CHEST){ player.x = nx; player.y = ny; gameWon = true; showFinal(); return; }
player.x = nx; player.y = ny;
+ pickupPowerup();
checkPlayerEnemyCollision();
}
function onDoorSolved(id){
@@ -274,6 +320,7 @@ function draw(now){
else if (t === T_CHEST){ drawFloor(px, py, x, y, isExp); drawChest(px, py); }
else drawFloor(px, py, x, y, isExp);
}
+ for (var pu = 0; pu < powerups.length; pu++) drawPowerup(powerups[pu], now);
for (var bi = 0; bi < bombs.length; bi++) drawBomb(bombs[bi], now);
for (var en = 0; en < enemies.length; en++) if (enemies[en].alive) drawEnemy(enemies[en]);
if (player.alive) drawPlayer(now);
@@ -284,6 +331,7 @@ function drawBox(px, py, isExp){ if (isExp){ ctx.fillStyle = '#f97316'; ctx.fill
function drawDoor(px, py){ ctx.fillStyle = '#9f1239'; ctx.fillRect(px+3, py+2, TS-6, TS-4); ctx.fillStyle = '#e11d48'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+TS/2-2, 4, 7); }
function drawChest(px, py){ ctx.fillStyle = '#92400e'; ctx.fillRect(px+4, py+8, TS-8, TS-14); ctx.fillStyle = '#f59e0b'; ctx.fillRect(px+4, py+8, TS-8, 8); ctx.fillStyle = '#fde68a'; ctx.fillRect(px+TS/2-3, py+11, 6, 10); ctx.fillStyle = '#fbbf24'; ctx.fillRect(px+TS/2-2, py+13, 4, 4); }
function drawBomb(bomb, now){ var px = bomb.x * TS, py = bomb.y * TS; var pulse = Math.sin(now / 150) * 0.3 + 0.7; ctx.fillStyle = 'rgba(30,10,10,' + pulse + ')'; ctx.beginPath(); ctx.arc(px + TS/2, py + TS/2, TS/2 - 4, 0, Math.PI*2); ctx.fill(); ctx.strokeStyle = '#f87171'; ctx.lineWidth = 2; ctx.stroke(); ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(px + TS/2 + 4, py + 6); ctx.quadraticCurveTo(px + TS/2 + 10, py + 2, px + TS/2 + 7, py - 2); ctx.stroke(); }
+function drawPowerup(p, now){ var px = p.x * TS, py = p.y * TS, cx = px + TS/2, cy = py + TS/2, pulse = Math.sin(now / 200) * 0.12 + 0.88; var isR = p.type === P_RANGE; ctx.fillStyle = isR ? 'rgba(249,115,22,.25)' : 'rgba(59,130,246,.25)'; ctx.fillRect(px + 3, py + 3, TS - 6, TS - 6); ctx.fillStyle = isR ? '#f97316' : '#3b82f6'; ctx.beginPath(); ctx.arc(cx, cy, (TS/2 - 6) * pulse, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = '#fff'; if (isR){ ctx.beginPath(); ctx.moveTo(cx, cy - 7); ctx.lineTo(cx + 5, cy + 6); ctx.lineTo(cx, cy + 2); ctx.lineTo(cx - 5, cy + 6); ctx.closePath(); ctx.fill(); } else { ctx.beginPath(); ctx.arc(cx, cy + 1, 5, 0, Math.PI*2); ctx.fill(); ctx.fillRect(cx - 1, cy - 8, 2, 4); } }
function drawEnemy(e){ var px = e.x * TS, py = e.y * TS; ctx.fillStyle = '#dc2626'; ctx.fillRect(px+6, py+8, TS-12, TS-12); ctx.fillStyle = '#fff'; ctx.fillRect(px+9, py+12, 4, 4); ctx.fillRect(px+TS-13, py+12, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+10, py+13, 2, 2); ctx.fillRect(px+TS-12, py+13, 2, 2); ctx.fillStyle = '#b91c1c'; ctx.fillRect(px+8, py+TS-8, 4, 5); ctx.fillRect(px+TS-12, py+TS-8, 4, 5); }
function drawPlayer(now){ var px = player.x * TS, py = player.y * TS; if (player.invincible && Math.floor(now / 100) % 2 === 0) return; ctx.fillStyle = CFG.color || '#6d28d9'; ctx.fillRect(px+6, py+5, TS-12, TS-10); ctx.fillStyle = '#fff'; ctx.fillRect(px+10, py+10, 4, 4); ctx.fillRect(px+TS-14, py+10, 4, 4); ctx.fillStyle = '#000'; ctx.fillRect(px+11, py+11, 2, 2); ctx.fillRect(px+TS-13, py+11, 2, 2); ctx.fillStyle = '#fff'; ctx.fillRect(px+11, py+TS-12, TS-22, 3); }
@@ -303,6 +351,10 @@ window.__game = {
get enemiesCount(){ return enemies ? enemies.filter(function(e){ return e.alive; }).length : 0; },
get puzzleProgress(){ return puzzleProgress; },
get bombs(){ return bombs ? bombs.slice() : []; },
+ get powerups(){ return powerups ? powerups.slice() : []; },
+ get bombRange(){ return bombRange; },
+ get maxBombs(){ return maxBombs; },
+ dropPowerupAt: function(x, y, type){ powerups.push({ x: x, y: y, type: type || P_RANGE }); },
get gameOver(){ return gameOver; },
get gameWon(){ return gameWon; },
get player(){ return player ? { x: player.x, y: player.y, alive: player.alive, invincible: player.invincible } : null; },
diff --git a/tests/AGENTS.md b/tests/AGENTS.md
index c6021bb..57a78b2 100644
--- a/tests/AGENTS.md
+++ b/tests/AGENTS.md
@@ -5,7 +5,7 @@ Smoke + regresie + campanie E2E pentru jocurile generate. Verifică faptic: fiec
până la ecranul final, fără erori de consolă.
## Ownership
-- `tests/smoke.mjs` — unicul fișier de teste (~26 teste).
+- `tests/smoke.mjs` — unicul fișier de teste (~27 teste).
- `playwright.config.mjs` (la root, **gitignored**) — config dev.
## Local Contracts
@@ -16,11 +16,11 @@ până la ecranul final, fără erori de consolă.
HTML temp generat via builder (`gameHTML`) și-l încarcă de pe `file://`.
- **Zero erori consolă = invariant.** `trackErrors(page)` colectează `console.error` + `pageerror`;
fiecare test asertează `errors.length === 0` la final.
-- **Tag-uri:** `@regresie` (14 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
- bomberman gameplay) și `@campanie` (12 — intro→hartă→camere→final, resume, cameră moartă,
- idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion,
- navigare overworld).
-- **Status țintă: 26/26 PASS.**
+- **Tag-uri:** `@regresie` (15 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
+ bomberman gameplay + bomberman rază/powerup-uri) și `@campanie` (12 — intro→hartă→camere→final, resume,
+ cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10,
+ a11y tap/aria/reduced-motion, navigare overworld).
+- **Status țintă: 27/27 PASS.**
## Work Guidance
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă
@@ -29,7 +29,7 @@ până la ecranul final, fără erori de consolă.
## Verification
```bash
-npx playwright test tests/smoke.mjs # 26/26
+npx playwright test tests/smoke.mjs # 27/27
npx playwright test tests/smoke.mjs --grep @regresie
npx playwright test tests/smoke.mjs --grep @campanie
```
diff --git a/tests/smoke.mjs b/tests/smoke.mjs
index e1f83c4..c1eb09e 100644
--- a/tests/smoke.mjs
+++ b/tests/smoke.mjs
@@ -1190,4 +1190,59 @@ test.describe('Campanie E2E @campanie', () => {
expect(errors, errors.join('\n')).toHaveLength(0);
});
+ test('arcade bomberman — raza initiala 1 + powerup-uri (drop la cutie, pickup creste raza/bombe) @regresie',
+ async ({ page }) => {
+ const errors = trackErrors(page);
+ await page.goto(fileURL('exemplu-arcade.html'));
+ await page.waitForFunction(() => typeof window.__game !== 'undefined', { timeout: 5000 });
+ await page.evaluate(() => window.__game.restartWithSeed(11));
+
+ // Raza initiala = 1, max bombe = 1
+ const base = await page.evaluate(() => ({ r: window.__game.bombRange, m: window.__game.maxBombs }));
+ expect(base.r, 'raza initiala trebuie sa fie 1').toBe(1);
+ expect(base.m, 'numar bombe initial trebuie sa fie 1').toBe(1);
+
+ // Raza 1: cutie la distanta 2 ramane intacta (player mutat departe de explozie)
+ await page.evaluate(() => {
+ window.__game.teleportPlayer(1, 1);
+ window.__game.setTile(2, 1, 2); window.__game.setTile(3, 1, 2);
+ window.__game.placeBomb(); window.__game.teleportPlayer(11, 11); window.__game.explodeAllBombs();
+ });
+ expect(await page.evaluate(() => window.__game.getTile(2, 1)), 'cutia la distanta 1 trebuie spartá').toBe(0);
+ expect(await page.evaluate(() => window.__game.getTile(3, 1)), 'raza 1: cutia la distanta 2 trebuie intactá').toBe(2);
+
+ // Drop: peste multe cutii sparte apar powerup-uri care SUPRAVIETUIESC exploziei care le-a creat.
+ // (Bug-ul prins: powerup-ul cadea pe celula cutiei si checkExplosionHits il stergea instant.)
+ const dropRounds = await page.evaluate(() => {
+ window.__game.restartWithSeed(11);
+ let rounds = 0;
+ for (let i = 0; i < 60; i++) {
+ window.__game.teleportPlayer(1, 1); window.__game.setTile(2, 1, 2);
+ window.__game.placeBomb(); window.__game.teleportPlayer(11, 11); window.__game.explodeAllBombs();
+ // ridica orice powerup ramas (mut player pe el) ca sa nu fie sters de explozia urmatoare
+ window.__game.powerups.slice().forEach(p => { if (p.x === 2 && p.y === 1) rounds++; window.__game.teleportPlayer(p.x, p.y); window.__game.movePlayer('R'); window.__game.movePlayer('L'); });
+ }
+ return rounds;
+ });
+ expect(dropRounds, 'niciun powerup nu a supravietuit pe celula cutiei sparte').toBeGreaterThan(0);
+
+ // Pickup: range creste bombRange, bomb creste maxBombs; powerup consumat
+ const pick = await page.evaluate(() => {
+ window.__game.restartWithSeed(11);
+ window.__game.setTile(2, 1, 0); window.__game.setTile(3, 1, 0);
+ window.__game.teleportPlayer(1, 1);
+ window.__game.dropPowerupAt(2, 1, 'range'); window.__game.movePlayer('R');
+ const a = { r: window.__game.bombRange, pu: window.__game.powerups.length };
+ window.__game.dropPowerupAt(3, 1, 'bomb'); window.__game.movePlayer('R');
+ const c = { m: window.__game.maxBombs, pu: window.__game.powerups.length };
+ return { a, c };
+ });
+ expect(pick.a.r, 'pickup range nu a crescut bombRange').toBe(2);
+ expect(pick.a.pu, 'powerup range nu a fost consumat').toBe(0);
+ expect(pick.c.m, 'pickup bomb nu a crescut maxBombs').toBe(2);
+ expect(pick.c.pu, 'powerup bomb nu a fost consumat').toBe(0);
+
+ expect(errors, errors.join('\n')).toHaveLength(0);
+ });
+
});