Giochi di carte dopo la revisione di Codex sui tre insieme; player dei video «Come si gioca» e primo copione
Esito che vale e si salva appena deciso, per tutti i minigiochi (anche tornando al titolo dalla schermata del risultato); briscola: uscire durante il conteggio non perde più la vittoria; tressette: si esce ai 21 col primo evento che ci porta, accusa compresa (prima vinceva sempre Andrea se ci arrivavano tutte e due), il carico che fa vincere si dà anche se è il 2, accuse che non escono dal tavolo, convenzioni scritte nell'aiuto; scopa: primiera valutata per quel che migliora, finale esatto a mazzo finito, presa sulla tavola al rilascio con scelta e conferma, frecce e Invio sempre sulla presa scelta. Tastiera: Invio e Spazio giocano sempre la carta scelta. Tavola descritta a parole. Classi CSS comuni ai tre giochi. Video: player nel gioco (pulsante «Guarda il video» nel «Come si gioca», compare quando il video esiste), video/narra.py per la voce del narratore, pezzi comuni dei copioni e il copione della briscola; scaletta in video/SCALETTA-CARTE.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
@@ -160,7 +160,7 @@ try {
|
||||
await page.evaluate(async () => {
|
||||
const { settle } = await import('/tests/helpers.mjs'); game.act('talk', 'giocatore_carte'); settle(game);
|
||||
});
|
||||
await page.locator('#choices li', { hasText: 'La rivincita?' }).click();
|
||||
await page.locator('#choices li', { hasText: 'Rivincita a briscola?' }).click();
|
||||
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
||||
await page.locator('#choices li', { hasText: 'Con le toscane.' }).click();
|
||||
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
||||
|
||||
+24
-2
@@ -60,7 +60,7 @@ test('l’ultima pescata è la briscola, poi tre prese senza mazzo; totale e con
|
||||
}
|
||||
assert.ok(lastDraw); assert.equal(c.step, 'count'); assert.equal(c.history.length, 20);
|
||||
assert.equal(c.hands.player.length + c.hands.cpu.length, 0); assert.equal(c.stock.length, 0);
|
||||
assert.equal(c.result, undefined, 'risultato solo dopo il conteggio');
|
||||
assert.equal(c.result.player + c.result.cpu, 120, 'il risultato è già deciso: il conteggio lo mostra soltanto');
|
||||
assert.equal(new Set([...c.taken.player, ...c.taken.cpu].map(a => a.id)).size, 40);
|
||||
assert.deepEqual(counted(c), { player: 0, cpu: 0 });
|
||||
update(c, 0.36); assert.equal(c.count, 1);
|
||||
@@ -165,7 +165,7 @@ test('oratorio: dialogo, livelli, abbandono, risultati, rivincita e salvataggio
|
||||
g.closeupClose(); settle(g); assert.equal(!!g.state.flags.briscola_vinta, outcome === 'win');
|
||||
assert.equal(!!g.state.flags.briscola_giocata, outcome !== 'quit');
|
||||
g.act('talk', 'giocatore_carte'); settle(g);
|
||||
assert.equal(g.options()[0].text, outcome === 'quit' ? 'Una briscola?' : 'La rivincita?');
|
||||
assert.equal(g.options()[0].text, outcome === 'quit' ? 'Una briscola?' : 'Rivincita a briscola?');
|
||||
assert.ok(MINIGAMES.bocce.press && MINIGAMES.biliardo.press);
|
||||
}
|
||||
});
|
||||
@@ -181,3 +181,25 @@ test('due mazzi, stesse regole: le napoletane cambiano solo i nomi', () => {
|
||||
// L'avversario ragiona sul mazzo giusto: le carte non viste sono napoletane.
|
||||
assert.ok(unseen(observation(n)).every(a => DECKS.napoletane.suits.includes(a.suit)));
|
||||
});
|
||||
|
||||
// Codex (revisione dei tre giochi): l'esito vale appena è deciso. Uscire durante il conteggio della briscola non lo
|
||||
// perde, e i flag finiscono nel salvataggio anche se dal risultato si torna al titolo senza uscire dal tavolo.
|
||||
test('carte all\'oratorio: l\'esito si salva appena deciso, e uscire durante il conteggio non lo perde', () => {
|
||||
const games = [['Una briscola?', 'briscola', 'briscola_vinta'], ['Un tressette ai 21?', 'tressette', 'tressette_vinto'], ['Una scopa?', 'scopa', 'scopa_vinta']];
|
||||
for (const [ask, id, flag] of games) {
|
||||
const saved = [], g = new Game(story, { onSave: s => saved.push(s) });
|
||||
settle(g); g.go('oratorio'); settle(g);
|
||||
g.act('talk', 'giocatore_carte'); settle(g);
|
||||
for (const text of [ask, 'Con le toscane.', 'Seria.']) { g.choose(g.options().findIndex(o => o.text === text)); settle(g); }
|
||||
assert.equal(g.closeup?.id, id);
|
||||
// La briscola durante il conteggio: il risultato c'è già. Gli altri due, a risultato.
|
||||
Object.assign(g.closeup, id === 'briscola' ? { step: 'count', result: { winner: 'player', player: 70, cpu: 50 } } : { step: 'result', result: { winner: 'player' } });
|
||||
g.update(0.01);
|
||||
assert.equal(saved.at(-1).flags[flag], true, `${id}: la vittoria è già nel salvataggio`);
|
||||
assert.equal(g.state.flags[flag], true);
|
||||
g.closeupClose(); const said = settle(g).map(s => s.text);
|
||||
const win = story.closeups[id].win.find(e => e.say).say, quit = story.closeups[id].quit[0].say;
|
||||
assert.ok(said.includes(win) && !said.includes(quit), `${id}: esce con la battuta della vittoria, non con quella di chi lascia`);
|
||||
assert.equal(g.state.flags[flag], true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -90,13 +90,17 @@ try {
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'choose');
|
||||
await page.waitForSelector('#scopa-options:not([hidden])');
|
||||
await page.screenshot({ path: `${out}/scopa-${name}-scelta.png` });
|
||||
if (mode === 'keyboard') { await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); }
|
||||
// Tastiera (Codex): fuoco sulla prima presa, freccia a destra, Invio: deve partire la seconda.
|
||||
if (mode === 'keyboard') { await page.locator('#scopa-options button:first-child').focus(); await page.keyboard.press('ArrowRight'); await page.keyboard.press('Enter'); }
|
||||
else if (mode === 'touch') {
|
||||
// Il 5 in tavola sta solo nella presa 2+5: toccarlo la sceglie.
|
||||
// Il 5 in tavola sta solo nella presa 2+5.
|
||||
const p = await page.evaluate(async () => {
|
||||
const { tableXY } = await import('/src/engine/scopa-render.js'), c = game.closeup;
|
||||
const i = c.table.findIndex(a => a.value === 5), [x, y] = tableXY(c.table.length, i); return [x + 21, y + 30];
|
||||
});
|
||||
// Il primo tocco sceglie la presa che lo contiene (evidenziata), il secondo la conferma.
|
||||
await at(page, mode, p);
|
||||
assert.equal(await page.evaluate(() => game.closeup.step === 'choose' && game.closeup.choice.selected), 1);
|
||||
await at(page, mode, p);
|
||||
} else await tap(page, mode, '#scopa-options button:nth-child(2)');
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'take');
|
||||
|
||||
@@ -101,6 +101,23 @@ test('l\'avversario prende il settebello e la scopa quando può, e non lascia sc
|
||||
const d = choose(x, 'male'); assert.equal(x.hand[d.index].rank, 'Regio', 'col 5 lascerebbe 7 in tavola: meglio il regio (12)');
|
||||
});
|
||||
|
||||
test('la primiera conta quel che migliora: col 6 si prende quello del seme dove si ha solo una figura (Codex)', () => {
|
||||
const v = { who: 'cpu', deck: 'toscane', hand: [card('picche', '6')], table: [card('cuori', '6'), card('fiori', '6')],
|
||||
taken: { player: [], cpu: [card('cuori', '7'), card('fiori', 'Regio')] }, opponentCount: 3, stock: 20, scope: { player: 0, cpu: 0 } };
|
||||
for (const level of ['seria', 'male']) assert.equal(choose(v, level).captured[0].suit, 'fiori', level);
|
||||
});
|
||||
|
||||
test('a mazzo finito «A fassi male» calcola il finale esatto: meglio perdere il settebello che regalare anche la scopa', () => {
|
||||
const deck = DECK_CARDS.toscane, seen = [card('mattoni', '7'), card('cuori', '7'), card('fiori', '4'), card('picche', '3')];
|
||||
// Andrea ha in mano l'ultimo 7. Posando il 4, Andrea prende 3+4 e fa scopa, e il settebello gli resta comunque;
|
||||
// posando il settebello, Andrea lo prende col 7 ma niente scopa.
|
||||
const v = { who: 'cpu', deck: 'toscane', hand: [card('fiori', '4'), card('mattoni', '7')], table: [card('picche', '3')], stock: 0,
|
||||
taken: { player: deck.filter(a => !seen.includes(a)).slice(0, 18), cpu: deck.filter(a => !seen.includes(a)).slice(18) },
|
||||
opponentCount: 1, scope: { player: 0, cpu: 0 }, lastTaker: 'player' };
|
||||
assert.deepEqual(unseen(v).map(a => a.id), [card('cuori', '7').id], 'le carte dell\'altro sono quelle non viste');
|
||||
assert.equal(v.hand[choose(v, 'male').index].rank, '7');
|
||||
});
|
||||
|
||||
test('taratura: Alla bona < Seria < A fassi male', () => {
|
||||
const runs = calibrate(60);
|
||||
assert.ok(runs[0].playerPercent > runs[1].playerPercent, JSON.stringify(runs));
|
||||
|
||||
@@ -94,6 +94,12 @@ try {
|
||||
});
|
||||
if (bad >= 0) {
|
||||
const held = await page.evaluate(() => game.closeup.hands[0].length);
|
||||
// Tastiera (Codex): fuoco su una carta buona, poi si sceglie col numero quella che non risponde e
|
||||
// Invio. Non deve partire la carta che aveva il fuoco.
|
||||
if (mode === 'keyboard') {
|
||||
const good = await page.evaluate(async () => (await import('/src/engine/tressette.js')).legal(game.closeup.hands[0], game.closeup.table)[0]);
|
||||
await page.locator(`#tressette-card-${good}`).focus();
|
||||
}
|
||||
await playCard(page, mode, bad);
|
||||
assert.match(await page.evaluate(() => game.closeup.refused), /Devi rispondere/);
|
||||
await page.waitForFunction(() => /Devi rispondere/.test(document.getElementById('tressette-status').textContent));
|
||||
|
||||
@@ -91,8 +91,12 @@ test('si va ai 21: chi ci arriva per primo è fuori, anche a metà smazzata, e l
|
||||
d.pending = [[], [{ who: 1, kind: 'napoletana', suit: 'fiori', points: 3, cards: [] }], [], []];
|
||||
d.hands[1] = [card('fiori', '7'), ...d.hands[1].slice(1)];
|
||||
assert.ok(play(d, 0, 1)); assert.equal(d.score[1], 22);
|
||||
for (const seat of [2, 3, 0]) { d.current = seat; play(d, choose(observation(d, seat), 'seria').index, seat); }
|
||||
assert.equal(d.step, 'trick'); collect(d); assert.equal(d.step, 'result'); assert.equal(d.result.winner, 'cpu');
|
||||
assert.equal(d.step, 'result', 'l\'accusa fa uscire subito'); assert.equal(d.result.winner, 'cpu');
|
||||
// Tutte e due a un passo, tutte e due con un'accusa: esce chi accusa per primo, chiunque sia (Codex, seme 28).
|
||||
const e = newTressette(28); e.base = [18, 18]; e.score = [18, 18]; e.current = 1; e.table = []; e.tricks = [];
|
||||
e.pending = [[{ who: 0, kind: 'buongioco', rank: '3', points: 3, cards: [] }], [{ who: 1, kind: 'buongioco', rank: '2', points: 3, cards: [] }], [], []];
|
||||
assert.ok(play(e, 0, 1)); assert.equal(e.result.winner, 'cpu'); assert.deepEqual(e.score, [18, 21]);
|
||||
assert.equal(play(e, 0, 2), false, 'a partita finita non si gioca più');
|
||||
});
|
||||
|
||||
test('partite intere deterministiche col seme; i due mazzi danno le stesse carte', () => {
|
||||
@@ -146,6 +150,15 @@ test('chi riceve il busso prende con la migliore; il socio non spreca il 3 su un
|
||||
assert.equal(d.hands[3][choose(observation(d, 3), 'male').index].rank, '5');
|
||||
});
|
||||
|
||||
test('se un carico al socio basta per i 21, si dà anche il 2', () => {
|
||||
const c = newTressette(8); c.pending = [[], [], [], []]; c.current = 3;
|
||||
c.base = [0, 20]; c.score = [0, 20]; c.thirds = [0, 1];
|
||||
c.table = [{ who: 0, card: card('cuori', '4') }, { who: 1, card: card('cuori', '3') }, { who: 2, card: card('cuori', '5') }];
|
||||
c.hands[3] = hand([['fiori', '2'], ['picche', '4']]);
|
||||
assert.equal(c.hands[3][choose(observation(c, 3), 'seria').index].rank, '2');
|
||||
assert.equal(c.hands[3][choose(observation(c, 3), 'male').index].rank, '2');
|
||||
});
|
||||
|
||||
test('chi vola ha finito il seme: per «A fassi male» non può più avere il 3', () => {
|
||||
const c = newTressette(8); c.pending = [[], [], [], []]; c.current = 1;
|
||||
c.tricks = [{ leader: 2, winner: 0, cards: [{ who: 2, card: card('cuori', '4'), signal: 'volo' }, { who: 3, card: card('cuori', '5') }, { who: 0, card: card('cuori', '6') }, { who: 1, card: card('cuori', '7') }] }];
|
||||
|
||||
Reference in New Issue
Block a user