Scopa agli 11 all'oratorio, e il tressette rivisto dopo la revisione critica di Codex
Scopa: toscane o napoletane (mattoni o denari), presa obbligatoria, la carta uguale prima delle somme, scelta fra più prese toccando la tavola, coi pulsanti o con frecce e Invio; scope (non all'ultima carta), carte, mattoni, settebello, primiera; tre livelli tarati (92, 64, 52%); test e prova nel browser. Tressette: fuori ai 21 presa per presa, vittoria subito definitiva (Esc non la cancella più), accuse dette al proprio turno e col seme che manca del tris, striscio e busso meno rigidi, il socio risponde al busso, tiene il 3 e il 2, non esce d'Asso scoperto, legge il volo; tastiera col fuoco sulla carta scelta, tavola descritta a parole, riepilogo che aspetta Andrea. Stesso Esc corretto anche nella scopa. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1dxvtsZx8NE9fwjXUNDBr
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// Scopa ai 11 nel browser: carte con mouse, tocco e tastiera, scelta fra più prese; si accelerano solo le attese.
|
||||
// npm run browser:scopa — /tmp/regolo-shots/scopa-*.png
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'node:fs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { staticServer } from './helpers.mjs';
|
||||
|
||||
const out = '/tmp/regolo-shots'; mkdirSync(out, { recursive: true });
|
||||
const server = await staticServer(), errors = [];
|
||||
let browser;
|
||||
async function tap(page, mode, selector) {
|
||||
if (mode === 'touch') { const r = await page.locator(selector).boundingBox(); await page.touchscreen.tap(r.x + r.width / 2, r.y + r.height / 2); }
|
||||
else await page.click(selector);
|
||||
}
|
||||
async function at(page, mode, [px, py]) {
|
||||
// Sul telefono i pulsanti stanno sotto il tavolo: dopo averli toccati la pagina può essere scesa.
|
||||
await page.locator('#scene').scrollIntoViewIfNeeded();
|
||||
const r = await page.locator('#scene').boundingBox(), x = r.x + px / 640 * r.width, y = r.y + py / 360 * r.height;
|
||||
if (mode === 'touch') await page.touchscreen.tap(x, y); else await page.mouse.click(x, y);
|
||||
}
|
||||
async function playCard(page, mode, index) {
|
||||
if (mode === 'keyboard') {
|
||||
await page.keyboard.press(String(index + 1));
|
||||
assert.equal(await page.evaluate(() => game.closeup.selected), index);
|
||||
await page.keyboard.press('Enter');
|
||||
return;
|
||||
}
|
||||
const p = await page.evaluate(async i => {
|
||||
const { handX, HAND } = await import('/src/engine/scopa-render.js');
|
||||
return [handX(game.closeup.hands.player.length, i) + 32, HAND.y + 46];
|
||||
}, index);
|
||||
await at(page, mode, p);
|
||||
}
|
||||
async function wait(page) {
|
||||
return page.evaluate(async () => {
|
||||
const { canPlay } = await import('/src/engine/scopa.js');
|
||||
// Un tocco mentre qualcuno parla salta la battuta, non gioca: la battuta si salta qui.
|
||||
for (let i = 0; i < 60; i++) {
|
||||
if (game.speech) game.skip();
|
||||
else if (canPlay(game.closeup) || ['smazzata', 'result', 'choose'].includes(game.closeup.step)) break;
|
||||
else game.update(1.4);
|
||||
}
|
||||
return game.closeup.step;
|
||||
});
|
||||
}
|
||||
try {
|
||||
browser = await chromium.launch({ args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader'] });
|
||||
const scenarios = [
|
||||
{ name: 'desktop-mouse', mode: 'mouse', viewport: { width: 1280, height: 860 }, full: true },
|
||||
{ name: 'telefono-touch', mode: 'touch', viewport: { width: 390, height: 844 } },
|
||||
{ name: 'desktop-keyboard', mode: 'keyboard', viewport: { width: 1280, height: 860 } },
|
||||
{ name: 'orizzontale-touch', mode: 'touch', viewport: { width: 844, height: 390 } },
|
||||
];
|
||||
for (const { name, mode, viewport, full } of scenarios) {
|
||||
const page = await browser.newPage({ viewport, hasTouch: true, isMobile: viewport.width < 900 });
|
||||
page.on('pageerror', e => errors.push(`${name}: ${e.message}`));
|
||||
page.on('console', e => { if (e.type() === 'error') errors.push(`${name}: ${e.text()}`); });
|
||||
await page.goto(`http://127.0.0.1:${server.port}/`);
|
||||
await page.click('#splash'); await page.click('#new-game');
|
||||
await page.click('#rione [data-rione="campomigliaio"]'); await page.click('#skip-intro');
|
||||
await page.evaluate(async () => { const { settle } = await import('/tests/helpers.mjs'); settle(game); game.go('oratorio'); settle(game); game.act('talk', 'giocatore_carte'); settle(game); });
|
||||
await page.locator('#choices li', { hasText: 'Una scopa?' }).click();
|
||||
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
||||
const deck = name.startsWith('telefono') ? 'napoletane' : 'toscane';
|
||||
await page.locator('#choices li', { hasText: deck === 'napoletane' ? 'Con le napoletane.' : 'Con le toscane.' }).click();
|
||||
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
||||
await page.locator('#choices li', { hasText: 'Seria.' }).click();
|
||||
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
||||
assert.deepEqual(await page.evaluate(() => [game.closeup.deck, game.closeup.level]), [deck, 'seria']);
|
||||
await page.waitForSelector('#scopa-controls:not([hidden])');
|
||||
assert.ok(await page.isHidden('#tressette-controls')); assert.ok(await page.isHidden('#briscola-controls'));
|
||||
await wait(page);
|
||||
await page.screenshot({ path: `${out}/scopa-${name}-inizio.png` });
|
||||
// Una volta per scenario: tavola preparata con due prese possibili per il 7, scelta toccando la tavola,
|
||||
// con un pulsante o con frecce e Invio.
|
||||
await page.evaluate(async () => {
|
||||
// Una partita nuova in cui esce Andrea, così in tavola ci sono proprio quattro carte.
|
||||
const { newScopa } = await import('/src/engine/scopa.js'), old = game.closeup;
|
||||
let seed = 1, c;
|
||||
do c = newScopa(seed++, { level: old.level, deck: old.deck }); while (c.current !== 'player');
|
||||
const pool = [...c.stock, ...c.table, c.hands.player[0]];
|
||||
const free = v => pool.splice(pool.findIndex(a => a.value === v), 1)[0];
|
||||
c.table = [free(3), free(4), free(2), free(5)]; c.hands.player[0] = free(7); c.stock = pool;
|
||||
game.closeup = { id: old.id, ...c };
|
||||
});
|
||||
// La tavola preparata non perde né duplica carte.
|
||||
const ids = await page.evaluate(() => { const c = game.closeup; return [...c.stock, ...c.table, ...c.hands.player, ...c.hands.cpu, ...c.taken.player, ...c.taken.cpu].map(a => a.id); });
|
||||
assert.equal(new Set(ids).size, 40); assert.equal(ids.length, 40);
|
||||
await playCard(page, mode, 0);
|
||||
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'); }
|
||||
else if (mode === 'touch') {
|
||||
// Il 5 in tavola sta solo nella presa 2+5: toccarlo la sceglie.
|
||||
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];
|
||||
});
|
||||
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');
|
||||
assert.deepEqual(await page.evaluate(() => game.closeup.last.captured.map(a => a.value).sort()), [2, 5]);
|
||||
await page.screenshot({ path: `${out}/scopa-${name}-presa.png` });
|
||||
let played = 1;
|
||||
for (let steps = 0; steps < 400; steps++) {
|
||||
const step = await wait(page);
|
||||
if (step === 'result') break;
|
||||
if (step === 'smazzata') {
|
||||
await page.screenshot({ path: `${out}/scopa-${name}-smazzata.png` });
|
||||
if (!full) break;
|
||||
if (mode === 'keyboard') await page.keyboard.press('Enter'); else await tap(page, mode, '#scopa-next');
|
||||
continue;
|
||||
}
|
||||
if (step !== 'play' && step !== 'choose') throw new Error('partita ferma in ' + step);
|
||||
if (step === 'choose') {
|
||||
if (mode === 'keyboard') await page.keyboard.press('Enter'); else await tap(page, mode, '#scopa-options button:first-child');
|
||||
continue;
|
||||
}
|
||||
const move = await page.evaluate(async () => {
|
||||
const { observation, choose } = await import('/src/engine/scopa.js');
|
||||
return choose(observation(game.closeup, 'player'), 'male', 3);
|
||||
});
|
||||
const before = await page.evaluate(() => game.closeup.hands.player.length);
|
||||
await playCard(page, mode, move.index); played++;
|
||||
const after = await page.evaluate(() => game.closeup.step === 'choose' ? -1 : game.closeup.hands.player.length);
|
||||
// Il gioco intanto va avanti da solo: dopo l'ultima carta la mano può essere già ridata.
|
||||
assert.ok(after === -1 || after === before - 1 || (before === 1 && after === 3), `un gesto gioca una carta (${name}: ${before} → ${after})`);
|
||||
}
|
||||
const count = await page.evaluate(() => game.closeup.summary ? game.closeup.summary.rows : null);
|
||||
assert.ok(count, 'conteggio della smazzata');
|
||||
if (full) {
|
||||
assert.equal(await page.evaluate(() => game.closeup.step), 'result');
|
||||
await page.screenshot({ path: `${out}/scopa-${name}-risultato.png` });
|
||||
const winner = await page.evaluate(() => game.closeup.result.winner);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.evaluate(async () => (await import('/tests/helpers.mjs')).settle(game));
|
||||
assert.equal(await page.evaluate(() => game.closeup), null);
|
||||
assert.equal(await page.evaluate(() => !!game.state.flags.scopa_vinta), winner === 'player');
|
||||
} else {
|
||||
await page.keyboard.press('Escape');
|
||||
assert.equal(await page.evaluate(() => game.closeup), null);
|
||||
assert.equal(await page.evaluate(() => !!game.state.flags.scopa_vinta), false);
|
||||
}
|
||||
console.log(`${name}: ${played} carte giocate, presa scelta fra due${full ? ', partita ai 11 finita' : ', una smazzata'} OK`);
|
||||
await page.close();
|
||||
}
|
||||
assert.deepEqual(errors, []); console.log(`Schermate: ${out}/scopa-*.png`);
|
||||
} finally { await browser?.close(); server.kill(); }
|
||||
@@ -0,0 +1,108 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { DECK_CARDS, COIN, newScopa, captures, primiera, tally, play, take, cancel, select, nudge, update, advance, finished, observation, unseen, choose, canPlay, status, TARGET } from '../src/engine/scopa.js';
|
||||
import { simulate, calibrate } from '../tools/calibrate_scopa.mjs';
|
||||
|
||||
const card = (suit, rank, deck = 'toscane') => DECK_CARDS[deck].find(a => a.suit === suit && a.rank === rank);
|
||||
|
||||
test('valori: Asso 1, numeri, gobbo 8, donna 9, regio 10; primiera 7 = 21, 6 = 18, Asso 16 … figure 10', () => {
|
||||
assert.deepEqual(['Asso', '5', 'Gobbo', 'Donna', 'Regio'].map(r => card('cuori', r).value), [1, 5, 8, 9, 10]);
|
||||
assert.deepEqual(['7', '6', 'Asso', '5', '4', '3', '2', 'Regio'].map(r => card('fiori', r).prime), [21, 18, 16, 15, 14, 13, 12, 10]);
|
||||
assert.equal(COIN.toscane, 'mattoni'); assert.equal(COIN.napoletane, 'denari');
|
||||
});
|
||||
|
||||
test('prese: la carta uguale ha la precedenza; se no ogni somma; più carte uguali, si sceglie', () => {
|
||||
const t = [card('cuori', '3'), card('fiori', '4'), card('picche', '7'), card('mattoni', '2'), card('cuori', '5')];
|
||||
assert.deepEqual(captures(t, card('mattoni', '7')).map(o => o.map(a => a.id)), [[card('picche', '7').id]], 'col 7 si prende il 7, non 3+4 o 2+5');
|
||||
const sums = captures([card('cuori', '3'), card('fiori', '4'), card('mattoni', '2'), card('cuori', '5')], card('picche', '7'));
|
||||
assert.equal(sums.length, 2, '3+4 oppure 2+5');
|
||||
assert.deepEqual(captures([card('cuori', '3'), card('fiori', '3')], card('picche', '3')).length, 2);
|
||||
assert.deepEqual(captures([card('cuori', '3')], card('picche', 'Donna')), []);
|
||||
});
|
||||
|
||||
test('primiera: la migliore per seme, e senza un seme non si fa', () => {
|
||||
const all = [card('cuori', '7'), card('mattoni', '6'), card('fiori', 'Asso'), card('picche', 'Regio'), card('picche', '2')];
|
||||
assert.equal(primiera(all, 'toscane'), 21 + 18 + 16 + 12);
|
||||
assert.equal(primiera(all.slice(0, 3), 'toscane'), 0);
|
||||
});
|
||||
|
||||
test('conteggio: carte, mattoni, settebello, primiera e scope; pari non si dà il punto', () => {
|
||||
const deck = DECK_CARDS.toscane, mine = deck.filter(a => a.suit === 'mattoni' || (a.suit === 'cuori' && a.value <= 2));
|
||||
const theirs = deck.filter(a => !mine.includes(a));
|
||||
const t = tally({ player: mine, cpu: theirs }, { player: 2, cpu: 0 }, 'toscane');
|
||||
const row = k => t.rows.find(r => r.key === k);
|
||||
assert.equal(row('carte').winner, 'cpu'); assert.equal(row('coin').winner, 'player'); assert.equal(row('coin').label, 'Mattoni');
|
||||
assert.equal(row('settebello').winner, 'player'); assert.equal(row('primiera').winner, null, 'a nessuno ci sono tutti e quattro i semi');
|
||||
assert.deepEqual(t.points, { player: 4, cpu: 1 });
|
||||
const half = tally({ player: deck.slice(0, 20), cpu: deck.slice(20) }, { player: 0, cpu: 0 }, 'napoletane');
|
||||
assert.equal(half.rows[0].winner, null, '20 a 20: le carte non le prende nessuno');
|
||||
assert.equal(tally({ player: [card('denari', '7', 'napoletane')], cpu: [] }, { player: 0, cpu: 0 }, 'napoletane').rows[2].winner, 'player');
|
||||
});
|
||||
|
||||
test('smazzata: 4 in tavola (mai tre re), tre a testa, la presa è obbligatoria, la scelta fra più prese', () => {
|
||||
for (let s = 1; s < 60; s++) assert.ok(newScopa(s).table.filter(a => a.value === 10).length < 3);
|
||||
const c = newScopa(3); c.current = 'player';
|
||||
assert.equal(c.table.length, 4); assert.equal(c.hands.player.length, 3); assert.equal(c.stock.length, 30);
|
||||
c.table = [card('cuori', '3'), card('fiori', '4'), card('mattoni', '2'), card('cuori', '5')];
|
||||
c.hands.player = [card('picche', '7'), card('picche', 'Regio')];
|
||||
assert.ok(play(c, 0)); assert.equal(c.step, 'choose'); assert.equal(c.choice.options.length, 2);
|
||||
assert.ok(nudge(c, 1)); assert.ok(cancel(c)); assert.equal(c.step, 'play'); assert.equal(c.hands.player.length, 2);
|
||||
assert.ok(play(c, 0)); assert.ok(select(c, 1)); assert.ok(take(c));
|
||||
assert.equal(c.step, 'take'); assert.equal(c.table.length, 2); assert.equal(c.taken.player.length, 3);
|
||||
});
|
||||
|
||||
test('scopa: chi pulisce la tavola fa un punto, ma non con l\'ultima carta della smazzata', () => {
|
||||
const c = newScopa(4); c.current = 'player'; c.table = [card('cuori', '3'), card('fiori', '4')];
|
||||
c.hands.player = [card('picche', '7'), card('picche', '2')];
|
||||
assert.ok(play(c, 0)); assert.equal(c.scope.player, 1); assert.equal(c.last.scopa, true); assert.match(status(c), /Scopa di Andrea/);
|
||||
const d = newScopa(4); d.current = 'player'; d.stock = []; d.hands.cpu = []; d.table = [card('cuori', '3')];
|
||||
d.hands.player = [card('picche', '3')];
|
||||
assert.ok(play(d, 0)); assert.equal(d.scope.player, 0, 'all\'ultima non è scopa');
|
||||
});
|
||||
|
||||
test('a fine smazzata le carte in tavola vanno all\'ultimo che ha preso; tutte le 40 carte si contano', () => {
|
||||
const c = newScopa(5, { level: 'seria' });
|
||||
while (c.step !== 'smazzata') {
|
||||
if (canPlay(c)) { const m = choose(observation(c, 'player'), 'seria'); assert.ok(play(c, m.index, 'player', m.option)); }
|
||||
update(c, 20);
|
||||
}
|
||||
assert.equal(c.taken.player.length + c.taken.cpu.length, 40);
|
||||
assert.equal(c.table.length, 0);
|
||||
const before = c.dealer;
|
||||
if (!finished(c)) { advance(c); assert.notEqual(c.dealer, before); assert.equal(c.current, before, 'gioca per primo chi non fa le carte'); }
|
||||
});
|
||||
|
||||
test('si va agli 11: a fine smazzata vince chi ne ha di più; pari, un\'altra; il risultato è subito definitivo', () => {
|
||||
const c = newScopa(6); c.stock = []; c.hands = { player: [], cpu: [card('cuori', '4')] };
|
||||
c.table = [card('fiori', '4')]; c.current = 'cpu'; c.score = { player: 9, cpu: 10 };
|
||||
c.taken = { player: DECK_CARDS.toscane.filter(a => a.suit !== 'fiori' || a.value !== 4).slice(0, 30), cpu: [] };
|
||||
assert.ok(play(c, 0, 'cpu')); update(c, 2);
|
||||
assert.equal(c.step, 'result', 'senza passare dal riepilogo: uscire non cancella la vittoria');
|
||||
assert.equal(c.result.winner, 'player');
|
||||
const d = newScopa(6); d.step = 'smazzata'; assert.ok(advance(d)); assert.equal(d.step, 'play');
|
||||
assert.ok(finished({ score: { player: TARGET, cpu: 9 } })); assert.ok(!finished({ score: { player: 12, cpu: 12 } }));
|
||||
});
|
||||
|
||||
test('partite deterministiche col seme; l\'avversario vede solo la sua mano, la tavola e le prese', () => {
|
||||
assert.deepEqual(simulate(31, 'male').score, simulate(31, 'male').score);
|
||||
const c = newScopa(12), v = observation(c, 'cpu');
|
||||
assert.equal(v.hands, undefined); assert.equal(v.stock, 30);
|
||||
assert.equal(unseen(v).length, 33, 'mano dell\'altro e mazzo');
|
||||
const other = structuredClone(c); other.hands.player.reverse(); other.stock.reverse();
|
||||
assert.deepEqual(choose(observation(c, 'cpu'), 'male', 3), choose(observation(other, 'cpu'), 'male', 3));
|
||||
});
|
||||
|
||||
test('l\'avversario prende il settebello e la scopa quando può, e non lascia scope facili', () => {
|
||||
const v = { who: 'cpu', deck: 'toscane', hand: [card('cuori', '7'), card('fiori', 'Regio')], table: [card('mattoni', '7'), card('picche', '4')], taken: { player: [], cpu: [] }, opponentCount: 3, stock: 20, scope: { player: 0, cpu: 0 } };
|
||||
const m = choose(v, 'seria'); assert.equal(v.hand[m.index].rank, '7', 'prende il settebello');
|
||||
const w = { ...v, hand: [card('cuori', '7'), card('fiori', '4')], table: [card('picche', '3'), card('mattoni', '4')] };
|
||||
const s = choose(w, 'male'); assert.equal(w.hand[s.index].rank, '7', 'scopa con 3+4');
|
||||
const x = { ...v, hand: [card('cuori', '5'), card('fiori', 'Regio')], table: [card('picche', '2')] };
|
||||
const d = choose(x, 'male'); assert.equal(x.hand[d.index].rank, 'Regio', 'col 5 lascerebbe 7 in tavola: meglio il regio (12)');
|
||||
});
|
||||
|
||||
test('taratura: Alla bona < Seria < A fassi male', () => {
|
||||
const runs = calibrate(60);
|
||||
assert.ok(runs[0].playerPercent > runs[1].playerPercent, JSON.stringify(runs));
|
||||
assert.ok(runs[1].playerPercent > runs[2].playerPercent, JSON.stringify(runs));
|
||||
});
|
||||
@@ -28,13 +28,20 @@ async function playCard(page, mode, index, signal) {
|
||||
const { handX, CARD } = await import('/src/engine/tressette-render.js');
|
||||
return [handX(game.closeup.hands[0].length, i) + 21, CARD.y + 30];
|
||||
}, index);
|
||||
// Sul telefono i pulsanti stanno sotto il tavolo: dopo averli toccati la pagina può essere scesa.
|
||||
await page.locator('#scene').scrollIntoViewIfNeeded();
|
||||
const r = await page.locator('#scene').boundingBox(), x = r.x + at[0] / 640 * r.width, y = r.y + at[1] / 360 * r.height;
|
||||
if (mode === 'touch') await page.touchscreen.tap(x, y); else await page.mouse.click(x, y);
|
||||
}
|
||||
async function wait(page) {
|
||||
return page.evaluate(async () => {
|
||||
const { canPlay } = await import('/src/engine/tressette.js');
|
||||
for (let i = 0; i < 12 && !canPlay(game.closeup) && !['smazzata', 'result'].includes(game.closeup.step); i++) game.update(1.3);
|
||||
// Un tocco mentre qualcuno parla salta la battuta, non gioca: la battuta si salta qui.
|
||||
for (let i = 0; i < 60; i++) {
|
||||
if (game.speech) game.skip();
|
||||
else if (canPlay(game.closeup) || ['smazzata', 'result'].includes(game.closeup.step)) break;
|
||||
else game.update(1.3);
|
||||
}
|
||||
return game.closeup.step;
|
||||
});
|
||||
}
|
||||
|
||||
+61
-24
@@ -1,6 +1,6 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { DECK_CARDS, newTressette, accuse, legal, best, signalOk, reason, setSignal, play, collect, advance, update, observation, choose, knowledge, canPlay, finished, team, status, TARGET } from '../src/engine/tressette.js';
|
||||
import { DECK_CARDS, newTressette, accuse, accuseName, legal, best, signalOk, reason, setSignal, play, collect, advance, update, observation, choose, knowledge, canPlay, finished, team, status, TARGET } from '../src/engine/tressette.js';
|
||||
import { simulate, calibrate } from '../tools/calibrate_tressette.mjs';
|
||||
|
||||
const card = (suit, rank, deck = 'toscane') => DECK_CARDS[deck].find(a => a.suit === suit && a.rank === rank);
|
||||
@@ -30,18 +30,22 @@ test('accuse: napoletana 3 punti, tre o quattro Assi, 2 o 3 per 3 o 4 punti, anc
|
||||
assert.deepEqual(a.map(x => [x.kind, x.points]), [['napoletana', 3], ['buongioco', 3]]);
|
||||
assert.equal(accuse(hand([['cuori', '2'], ['picche', '2'], ['fiori', '2'], ['mattoni', '2']]))[0].points, 4);
|
||||
assert.deepEqual(accuse(hand([['cuori', 'Asso'], ['cuori', '2'], ['picche', '3']])), []);
|
||||
// Del tris si dice il seme che manca, così Andrea sa quello che sa l'avversario.
|
||||
const tris = accuse(hand([['picche', '3'], ['fiori', '3'], ['mattoni', '3']]), ['cuori', 'mattoni', 'fiori', 'picche'])[0];
|
||||
assert.equal(tris.missing, 'cuori'); assert.equal(accuseName(tris), 'tre 3, manca cuori');
|
||||
});
|
||||
|
||||
test('segni onesti: volo con l\'ultima, striscio con almeno due rimaste, busso con almeno una; solo chi esce', () => {
|
||||
const h = hand([['cuori', '4'], ['cuori', '5'], ['cuori', '6'], ['fiori', '7']]);
|
||||
assert.ok(signalOk(h, 3, [], 'volo')); assert.ok(!signalOk(h, 0, [], 'volo'));
|
||||
assert.ok(signalOk(h, 0, [], 'striscio')); assert.ok(!signalOk(h, 3, [], 'striscio'));
|
||||
assert.ok(signalOk(h, 0, [], 'busso')); assert.ok(!signalOk(h, 3, [], 'busso'));
|
||||
test('segni: volo con l\'ultima, striscio se ne resta un\'altra, busso sempre; solo chi esce', () => {
|
||||
const h = hand([['cuori', '4'], ['cuori', '5'], ['fiori', '7']]);
|
||||
assert.ok(signalOk(h, 2, [], 'volo')); assert.ok(!signalOk(h, 0, [], 'volo'));
|
||||
assert.ok(signalOk(h, 0, [], 'striscio')); assert.ok(!signalOk(h, 2, [], 'striscio'));
|
||||
assert.ok(signalOk(h, 0, [], 'busso')); assert.ok(signalOk(h, 2, [], 'busso'));
|
||||
assert.ok(!signalOk(h, 0, [{ who: 1, card: card('cuori', '7') }], 'busso'));
|
||||
const c = newTressette(5); c.current = 0; c.table = []; c.hands[0] = [...h];
|
||||
const c = newTressette(5); c.current = 0; c.table = []; c.hands[0] = [...h]; c.pending[0] = [];
|
||||
assert.ok(setSignal(c, 'volo')); assert.equal(play(c, 0), false); assert.match(c.refused, /Volo solo/);
|
||||
assert.equal(c.hands[0].length, 4, 'la carta resta in mano');
|
||||
assert.ok(play(c, 3)); assert.equal(c.table[0].signal, 'volo'); assert.equal(c.signal, null);
|
||||
assert.equal(c.hands[0].length, 3, 'la carta resta in mano');
|
||||
assert.equal(play(c, 0, 0, 'xyz'), false); assert.equal(c.refused, 'Segno non riconosciuto.');
|
||||
assert.ok(play(c, 2)); assert.equal(c.table[0].signal, 'volo'); assert.equal(c.signal, null);
|
||||
assert.ok(c.events.some(e => e.kind === 'card-volo'));
|
||||
});
|
||||
|
||||
@@ -52,30 +56,43 @@ test('la carta che non risponde al seme si rifiuta con la ragione', () => {
|
||||
assert.ok(canPlay(c)); assert.ok(play(c, 0));
|
||||
});
|
||||
|
||||
test('una smazzata: 10 prese, 11 punti fra terzi e ultima presa, accuse nel punteggio, poi la seconda', () => {
|
||||
test('una smazzata: accuse dette al proprio turno, 10 prese, sempre 11 punti fra terzi e ultima presa', () => {
|
||||
const c = newTressette(1234, { level: 'seria' });
|
||||
const accused = [0, 1].map(t => c.accuse.filter(a => team(a.who) === t).reduce((n, a) => n + a.points, 0));
|
||||
assert.deepEqual(c.score, accused);
|
||||
assert.deepEqual(c.score, [0, 0]); assert.deepEqual(c.accuse, [], 'prima di giocare nessuno ha accusato');
|
||||
const owed = c.pending.flat();
|
||||
assert.equal(c.current, (c.dealer + 1) % 4, 'esce chi sta a destra del mazziere');
|
||||
assert.ok(c.hands.every(h => h.length === 10));
|
||||
const firstDealer = c.dealer;
|
||||
while (c.step !== 'smazzata') {
|
||||
while (!['smazzata', 'result'].includes(c.step)) {
|
||||
if (canPlay(c)) { const m = choose(observation(c, 0), 'seria'); assert.ok(play(c, m.index, 0, m.signal)); }
|
||||
update(c, 20);
|
||||
if (c.tricks.length === 1) assert.equal(c.accuse.length, owed.length, 'dopo la prima presa hanno accusato tutti');
|
||||
}
|
||||
if (c.step === 'smazzata') {
|
||||
assert.equal(c.tricks.length, 10);
|
||||
assert.equal(c.summary.thirds[0] + c.summary.thirds[1], 35);
|
||||
assert.equal(c.summary.points[0] + c.summary.points[1], 11, 'i terzi avanzati sommano sempre a due');
|
||||
assert.deepEqual(c.score, [0, 1].map(t => c.summary.accused[t] + c.summary.points[t]));
|
||||
assert.ok(advance(c)); assert.equal(c.dealer, (firstDealer + 1) % 4); assert.equal(c.step, 'play');
|
||||
}
|
||||
assert.equal(c.tricks.length, 10);
|
||||
assert.equal(c.summary.thirds[0] + c.summary.thirds[1], 35);
|
||||
assert.ok([10, 11].includes(c.summary.points[0] + c.summary.points[1]), 'si perdono al più i terzi avanzati');
|
||||
assert.deepEqual(c.score, [0, 1].map(t => accused[t] + c.summary.points[t]));
|
||||
if (!finished(c)) { advance(c); assert.equal(c.dealer, (firstDealer + 1) % 4); assert.equal(c.step, 'play'); }
|
||||
});
|
||||
|
||||
test('si va ai 21: vince chi ci arriva per primo; pari sopra i 21 si continua', () => {
|
||||
const c = newTressette(3);
|
||||
c.step = 'smazzata'; c.score = [21, 21]; assert.ok(!finished(c)); advance(c); assert.equal(c.step, 'play');
|
||||
c.step = 'smazzata'; c.score = [TARGET, 17]; advance(c); assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player');
|
||||
c.step = 'smazzata'; c.score = [22, 24]; advance(c); assert.equal(c.result.winner, 'cpu');
|
||||
assert.match(status(c), /Hanno vinto loro, 22 a 24/);
|
||||
test('si va ai 21: chi ci arriva per primo è fuori, anche a metà smazzata, e la vittoria non si perde', () => {
|
||||
const c = newTressette(3); c.pending = [[], [], [], []];
|
||||
c.base = [20, 19]; c.score = [20, 19]; c.thirds = [2, 0];
|
||||
// Una presa da un terzo porta noi a 21 (3 terzi = 1 punto): fuori subito.
|
||||
c.table = [{ who: 0, card: card('cuori', '3') }, { who: 1, card: card('cuori', '4') }, { who: 2, card: card('cuori', '5') }, { who: 3, card: card('cuori', '6') }];
|
||||
c.step = 'trick'; c.last = { winner: 0, cards: c.table.map(p => p.card) };
|
||||
assert.ok(collect(c)); assert.equal(c.step, 'result'); assert.equal(c.result.winner, 'player'); assert.deepEqual(c.score, [21, 19]);
|
||||
assert.ok(finished(c)); assert.equal(advance(c), false);
|
||||
assert.match(status(c), /Avete vinto voi, 21 a 19/);
|
||||
// Anche le accuse bastano: dette giocando, contano alla fine di quella presa.
|
||||
const d = newTressette(3); d.base = [0, 19]; d.score = [0, 19]; d.current = 1; d.table = []; d.tricks = [];
|
||||
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');
|
||||
});
|
||||
|
||||
test('partite intere deterministiche col seme; i due mazzi danno le stesse carte', () => {
|
||||
@@ -117,6 +134,26 @@ test('il socio ritorna il seme a chi ha bussato', () => {
|
||||
assert.equal(c.hands[2][m.index].suit, 'cuori'); assert.equal(c.hands[2][m.index].rank, '4');
|
||||
});
|
||||
|
||||
test('chi riceve il busso prende con la migliore; il socio non spreca il 3 su una presa già sua', () => {
|
||||
const c = newTressette(8); c.pending = [[], [], [], []]; c.current = 2;
|
||||
c.table = [{ who: 0, card: card('cuori', '4'), signal: 'busso' }, { who: 1, card: card('cuori', '5') }];
|
||||
c.hands[2] = hand([['cuori', '2'], ['cuori', '6'], ['fiori', '7']]);
|
||||
assert.equal(c.hands[2][choose(observation(c, 2), 'male').index].rank, '2', 'al busso risponde col 2');
|
||||
const d = newTressette(8); d.pending = [[], [], [], []]; d.current = 3;
|
||||
d.table = [{ who: 0, card: card('fiori', '6') }, { who: 1, card: card('fiori', '2') }, { who: 2, card: card('fiori', '4') }];
|
||||
d.hands[3] = hand([['fiori', '3'], ['fiori', '5'], ['cuori', '7']]);
|
||||
// L'avversario del posto 3: il suo socio (1) prende col 2 e lui è l'ultimo; tiene il 3 e dà il 5.
|
||||
assert.equal(d.hands[3][choose(observation(d, 3), 'male').index].rank, '5');
|
||||
});
|
||||
|
||||
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') }] }];
|
||||
c.hands[1] = hand([['cuori', '2'], ['fiori', '4']]); c.table = [];
|
||||
const k = knowledge(observation(c, 1), true);
|
||||
assert.equal(k.could(2, card('cuori', '3')), false);
|
||||
});
|
||||
|
||||
test('taratura: Alla bona < Seria < A fassi male, contro Andrea e il socio che giocano attenti', () => {
|
||||
const runs = calibrate(60);
|
||||
assert.ok(runs[0].playerPercent > runs[1].playerPercent, JSON.stringify(runs));
|
||||
|
||||
Reference in New Issue
Block a user