Adventure Mode v0 (E0-E6): ramificare per-raspuns in campanie
Flag opt-in `adventure` (default off) — zero regresie non-adventure.
E0: `adventure:false` in defaultState + checkbox builder + `ADVENTURE` in orchestrator.
E1: `_lastGiven` in libJS; `checkAnswer` captureaza raspunsul; `campaignDone`
calculeaza cheia branch ('*'/text-tf/index-choice) si o adauga in payload nextRoom.
E2: `resolveBranch(idx,key)` + rutare nextRoom: 'end'→owExitUnlocked+showOverworld;
numar→owUnlocked[dest]+showOverworld(dest). Non-adventure: comportament existent.
E3: `owCheckEnter` blocheaza usi incuiate (ADVENTURE&&!owUnlocked); exit via
owExitUnlocked. `owRefreshDoors`: stil `.locked` (dim+lock). `__ow.state`:
expune owUnlocked/owExitUnlocked.
E4: `saveProgress` adauga doneList+owUnlocked+owExitUnlocked+target; `tryResume`
reconstruieste din doneList non-contiguu (nu bucla liniara 0..idx).
E5: `buildDiploma`: ADVENTURE&&!roomDone[i] → 'neexplorata' (nu stele inselatoare).
E6: Builder UI — `normalizePuzzle` garanteaza p.branch={}; `cleanState` clampa
tintele+strip branch cand !adventure; `puzzleCard` afiseaza dropdown-uri
ramificare per-puzzle (free=1, tf=2, choice=1/optiune); `data-fb` handler;
`adventure` change → renderPuzzles().
Smoke 35/35 (4 teste noi: branch-jump, resume non-contiguu, regression, tf branch).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (~31 teste).
|
||||
- `tests/smoke.mjs` — unicul fișier de teste (~35 teste).
|
||||
- `playwright.config.mjs` (la root, **gitignored**) — config dev.
|
||||
|
||||
## Local Contracts
|
||||
@@ -18,9 +18,11 @@ până la ecranul final, fără erori de consolă.
|
||||
fiecare test asertează `errors.length === 0` la final.
|
||||
- **Tag-uri:** `@regresie` (16 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
|
||||
stil top-level invalid la import + bomberman gameplay + bomberman rază/powerup-uri) și `@campanie`
|
||||
(17 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil,
|
||||
audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10, muzica ambient T10, diploma A4).
|
||||
- **Status țintă: 31/31 PASS.**
|
||||
(21 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil,
|
||||
audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10,
|
||||
muzica ambient T10, diploma A4, adventure branch-jump, adventure resume non-contiguu,
|
||||
adventure regression non-adventure, adventure branch tf).
|
||||
- **Status țintă: 35/35 PASS.**
|
||||
|
||||
## Work Guidance
|
||||
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă
|
||||
@@ -29,7 +31,7 @@ până la ecranul final, fără erori de consolă.
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
npx playwright test tests/smoke.mjs # 31/31
|
||||
npx playwright test tests/smoke.mjs # 35/35
|
||||
npx playwright test tests/smoke.mjs --grep @regresie
|
||||
npx playwright test tests/smoke.mjs --grep @campanie
|
||||
```
|
||||
|
||||
205
tests/smoke.mjs
205
tests/smoke.mjs
@@ -1450,4 +1450,209 @@ test.describe('Campanie E2E @campanie', () => {
|
||||
expect(errors, errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Adventure Mode tests (E0-E6)
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Helper: genereaza cfg de campanie cu adventure ON. */
|
||||
function adventureCfg(puzzles) {
|
||||
return {
|
||||
title: 'Test Adventure', player: 'Tester', color: '#6d28d9',
|
||||
style: 'campaign', charName: 'Alex',
|
||||
story: 'Aventura de test.',
|
||||
finalMessage: 'Ai terminat aventura!',
|
||||
adventure: true,
|
||||
puzzles
|
||||
};
|
||||
}
|
||||
|
||||
test('adventure — branch-jump: room0→2 (sare room1), room2→exit, diploma neexplorata @campanie',
|
||||
async ({ page }) => {
|
||||
test.setTimeout(90000);
|
||||
const errors = trackErrors(page);
|
||||
const cfg = adventureCfg([
|
||||
{ title: 'Camera 0', type: 'free', question: 'R0?', answer: 'r0', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'A', style: 'classic', branch: { '*': 2 } },
|
||||
{ title: 'Camera 1', type: 'free', question: 'R1?', answer: 'r1', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'B', style: 'classic', branch: { '*': '' } },
|
||||
{ title: 'Camera 2', type: 'free', question: 'R2?', answer: 'r2', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'C', style: 'classic', branch: { '*': 'end' } }
|
||||
]);
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'adv-jump');
|
||||
|
||||
const gp = await page.context().newPage();
|
||||
const gameErrors = trackErrors(gp);
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
await gp.locator('#btn-start').click();
|
||||
|
||||
// Enter room 0 (door 0 unlocked in adventure)
|
||||
await enterRoom(gp, 0);
|
||||
await solveRoom(gp, 'classic', 'r0');
|
||||
|
||||
// After solving room 0: overworld with door 2 unlocked, door 1 locked
|
||||
await waitOverworld(gp);
|
||||
const stAfter0 = await gp.evaluate(() => window.__ow.state);
|
||||
expect(stAfter0.owUnlocked[2], 'door 2 trebuie deblocata dupa room0').toBeTruthy();
|
||||
expect(stAfter0.owUnlocked[1], 'door 1 trebuie sa ramana incuiata').toBeFalsy();
|
||||
expect(stAfter0.doors[0].solved, 'room 0 trebuie sa fie done').toBe(true);
|
||||
|
||||
// Door 1 should be locked — entering it should be blocked (stay in overworld)
|
||||
await gp.evaluate(() => window.__ow.enterDoor(1));
|
||||
await gp.waitForTimeout(300);
|
||||
const stLocked = await gp.evaluate(() => window.__ow.state);
|
||||
expect(stLocked.active, 'harta trebuie sa ramana activa cand usa e incuiata').toBe(true);
|
||||
|
||||
// Enter room 2 (unlocked)
|
||||
await enterRoom(gp, 2);
|
||||
await solveRoom(gp, 'classic', 'r2');
|
||||
|
||||
// After solving room 2: exit should be unlocked
|
||||
await waitOverworld(gp);
|
||||
const stAfter2 = await gp.evaluate(() => window.__ow.state);
|
||||
expect(stAfter2.owExitUnlocked, 'exit trebuie deblocat dupa room2→end').toBe(true);
|
||||
|
||||
// Enter exit → finale
|
||||
await gp.evaluate(() => window.__ow.enterExit());
|
||||
await gp.waitForFunction(
|
||||
() => document.getElementById('finale')?.classList.contains('show'),
|
||||
null, { timeout: 5000 }
|
||||
);
|
||||
|
||||
// Open diploma → camera 1 should be "neexplorata"
|
||||
await gp.locator('#btn-diploma').click();
|
||||
const diplomaText = await gp.locator('#dipl-rooms').innerText();
|
||||
expect(diplomaText).toMatch(/neexplorat/i);
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
|
||||
expect(gameErrors, 'Game errors:\n' + gameErrors.join('\n')).toHaveLength(0);
|
||||
expect(errors, 'Builder errors:\n' + errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('adventure — resume non-contiguu: room0 done → reload → room0 done + usa2 deblocata + usa1 incuiata @campanie',
|
||||
async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const errors = trackErrors(page);
|
||||
const cfg = adventureCfg([
|
||||
{ title: 'Camera 0', type: 'free', question: 'R0?', answer: 'r0', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'A', style: 'classic', branch: { '*': 2 } },
|
||||
{ title: 'Camera 1', type: 'free', question: 'R1?', answer: 'r1', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'B', style: 'classic', branch: { '*': '' } },
|
||||
{ title: 'Camera 2', type: 'free', question: 'R2?', answer: 'r2', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'C', style: 'classic', branch: { '*': 'end' } }
|
||||
]);
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'adv-resume');
|
||||
|
||||
const gp = await page.context().newPage();
|
||||
const gameErrors = trackErrors(gp);
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
await gp.locator('#btn-start').click();
|
||||
|
||||
// Solve room 0 → branches to room 2, skips room 1
|
||||
await enterRoom(gp, 0);
|
||||
await solveRoom(gp, 'classic', 'r0');
|
||||
await waitOverworld(gp);
|
||||
|
||||
// Reload — tryResume trebuie sa reconstituie starea non-contigua
|
||||
await gp.reload();
|
||||
await gp.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Asteapta overworld activ (resume, nu intro)
|
||||
await gp.waitForFunction(() => window.__ow && window.__ow.state.active, null, { timeout: 8000 });
|
||||
|
||||
const stResume = await gp.evaluate(() => window.__ow.state);
|
||||
expect(stResume.doors[0].solved, 'room 0 trebuie sa fie done dupa resume').toBe(true);
|
||||
expect(stResume.owUnlocked[2], 'usa 2 trebuie deblocata dupa resume').toBeTruthy();
|
||||
expect(stResume.owUnlocked[1], 'usa 1 trebuie sa ramana incuiata dupa resume').toBeFalsy();
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
|
||||
expect(gameErrors, 'Game errors:\n' + gameErrors.join('\n')).toHaveLength(0);
|
||||
expect(errors, 'Builder errors:\n' + errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('adventure off — regresia non-adventure: toate usile intrabile in orice ordine @campanie',
|
||||
async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const errors = trackErrors(page);
|
||||
// adventure:false (default) — toate ușile deblocate, orice ordine
|
||||
const cfg = campaignCfg(3, 'classic');
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'adv-off');
|
||||
|
||||
const gp = await page.context().newPage();
|
||||
const gameErrors = trackErrors(gp);
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
await gp.locator('#btn-start').click();
|
||||
|
||||
// In non-adventure: can enter door 1 first (not door 0)
|
||||
await waitOverworld(gp);
|
||||
await gp.evaluate(() => window.__ow.enterDoor(1));
|
||||
await gp.waitForFunction(
|
||||
() => document.getElementById('room-frame')?.hasAttribute('data-room-ready'),
|
||||
null, { timeout: 8000 }
|
||||
);
|
||||
// overworld became inactive (we entered a room) — confirms door 1 was enterable
|
||||
const stAfter = await gp.evaluate(() => window.__ow.state);
|
||||
expect(stAfter.active, 'harta trebuie sa fie inactiva dupa intrarea in room1').toBe(false);
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
|
||||
expect(gameErrors, 'Game errors:\n' + gameErrors.join('\n')).toHaveLength(0);
|
||||
expect(errors, 'Builder errors:\n' + errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('adventure — branch tf: raspuns Adevarat→2, Fals→1 deblocheza usa corecta @campanie',
|
||||
async ({ page }) => {
|
||||
test.setTimeout(60000);
|
||||
const errors = trackErrors(page);
|
||||
const cfg = adventureCfg([
|
||||
{ title: 'Camera 0', type: 'tf', question: 'E adevarat?', answer: '', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'A', style: 'classic', branch: { Adevarat: 2, Fals: 1 } },
|
||||
{ title: 'Camera 1', type: 'free', question: 'R1?', answer: 'r1', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'B', style: 'classic', branch: {} },
|
||||
{ title: 'Camera 2', type: 'free', question: 'R2?', answer: 'r2', tfAnswer: 'Adevarat', choices: '', hint: '', letter: 'C', style: 'classic', branch: {} }
|
||||
]);
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'adv-tf');
|
||||
|
||||
const gp = await page.context().newPage();
|
||||
const gameErrors = trackErrors(gp);
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
await gp.locator('#btn-start').click();
|
||||
|
||||
// Enter room 0 (tf puzzle, classic engine: buttons in #answers)
|
||||
await waitOverworld(gp);
|
||||
await gp.evaluate(() => window.__ow.enterDoor(0));
|
||||
await gp.waitForFunction(
|
||||
() => document.getElementById('room-frame')?.hasAttribute('data-room-ready'),
|
||||
null, { timeout: 8000 }
|
||||
);
|
||||
const ifl = gp.frameLocator('#room-frame');
|
||||
await ifl.locator('#btnStart').click();
|
||||
// Click "Adevarat" (correct answer → branch key 'Adevarat' → should unlock door 2)
|
||||
await ifl.locator('#answers button:text("Adevarat")').click();
|
||||
await gp.waitForTimeout(1200); // animatie next()
|
||||
|
||||
await waitOverworld(gp);
|
||||
const st = await gp.evaluate(() => window.__ow.state);
|
||||
expect(st.owUnlocked[2], 'Adevarat→2: usa 2 trebuie deblocata').toBeTruthy();
|
||||
expect(st.owUnlocked[1], 'Adevarat→2: usa 1 trebuie sa ramana incuiata').toBeFalsy();
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
|
||||
expect(gameErrors, 'Game errors:\n' + gameErrors.join('\n')).toHaveLength(0);
|
||||
expect(errors, 'Builder errors:\n' + errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user