import { test } from 'node:test'; import assert from 'node:assert/strict'; 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); const hand = list => list.map(([s, r]) => card(s, r)); test('mazzo: 40 carte, 32 terzi di punti, ordine 3 > 2 > Asso > figure > 7 … 4', () => { for (const cards of Object.values(DECK_CARDS)) { assert.equal(cards.length, 40); assert.equal(cards.reduce((n, a) => n + a.thirds, 0), 32); } const order = ['3', '2', 'Asso', 'Regio', 'Donna', 'Gobbo', '7', '6', '5', '4']; for (let i = 1; i < order.length; i++) assert.ok(card('cuori', order[i - 1]).power > card('cuori', order[i]).power, order[i]); assert.equal(card('picche', 'Asso').thirds, 3); assert.equal(card('picche', 'Regio').thirds, 1); assert.equal(card('picche', '7').thirds, 0); }); test('si risponde al seme; chi non ce l\'ha gioca quel che vuole; prende la più alta del seme di uscita', () => { const h = hand([['cuori', '4'], ['fiori', '3'], ['cuori', 'Regio']]); assert.deepEqual(legal(h, []), [0, 1, 2]); assert.deepEqual(legal(h, [{ who: 1, card: card('cuori', '7') }]), [0, 2]); assert.deepEqual(legal(h, [{ who: 1, card: card('picche', '7') }]), [0, 1, 2]); const table = [{ who: 1, card: card('cuori', '4') }, { who: 2, card: card('fiori', '3') }, { who: 3, card: card('cuori', '2') }, { who: 0, card: card('cuori', 'Asso') }]; assert.equal(best(table).who, 3, 'il 3 di fiori non risponde: prende il 2 di cuori'); }); test('accuse: napoletana 3 punti, tre o quattro Assi, 2 o 3 per 3 o 4 punti, anche insieme', () => { const h = hand([['cuori', 'Asso'], ['cuori', '2'], ['cuori', '3'], ['picche', '3'], ['fiori', '3'], ['mattoni', '7']]); const a = accuse(h); 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: 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]; 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, 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')); }); test('la carta che non risponde al seme si rifiuta con la ragione', () => { const c = newTressette(9); c.current = 0; c.hands[0] = hand([['cuori', '4'], ['fiori', '3']]); c.table = [{ who: 3, card: card('cuori', '7') }]; assert.equal(play(c, 1), false); assert.equal(reason(c, 1), 'Devi rispondere a cuori.'); assert.ok(canPlay(c)); assert.ok(play(c, 0)); }); test('una smazzata: accuse dette al proprio turno, 10 prese, sempre 11 punti fra terzi e ultima presa', () => { const c = newTressette(1234, { level: 'seria' }); 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 (!['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'); } }); 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); 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', () => { const a = simulate(77, 'male'), b = simulate(77, 'male'); assert.deepEqual(a.score, b.score); assert.ok(Math.max(...a.score) >= TARGET); const t = newTressette(42), n = newTressette(42, { deck: 'napoletane' }); assert.deepEqual(t.hands.map(h => h.map(a => a.id)), n.hands.map(h => h.map(a => a.id))); assert.equal(n.hands[0][0].suit === 'denari' || ['coppe', 'spade', 'bastoni'].includes(n.hands[0][0].suit), true); }); test('gli altri non vedono le carte altrui: l\'osservazione ha solo la propria mano e il tavolo', () => { const c = newTressette(11), v = observation(c, 1); assert.deepEqual(v.hand.map(a => a.id), c.hands[1].map(a => a.id)); assert.equal(v.hands, undefined); assert.equal(v.seed, undefined); const k = knowledge(v, true); assert.equal(k.out.length, 30, 'fuori ci sono le 30 carte degli altri tre'); assert.ok(!k.could(1, k.out[0]), 'non può pensare di averle lui'); // Cambiare le mani degli altri non cambia la scelta. const other = structuredClone(c); other.hands[0].reverse(); other.hands[2].reverse(); c.current = other.current = 1; assert.deepEqual(choose(observation(c, 1), 'male', 5), choose(observation(other, 1), 'male', 5)); }); test('«A fassi male» conta: sa che il 3 è uscito, e col 2 si fa la presa sicura', () => { const c = newTressette(8); c.current = 1; c.table = []; c.hands[1] = hand([['cuori', '2'], ['fiori', '4']]); c.tricks = [{ leader: 0, winner: 0, cards: [{ who: 0, card: card('cuori', '3') }, { who: 1, card: card('fiori', '5') }, { who: 2, card: card('cuori', '4') }, { who: 3, card: card('cuori', '5') }] }]; assert.equal(choose(observation(c, 1), 'male').index, 0, 'esce col 2, ormai il più alto'); const foes = [0, 2]; assert.equal(knowledge(observation(c, 1), false).beatable(card('cuori', '2'), foes), true, 'chi non conta ha paura del 3'); assert.equal(knowledge(observation(c, 1), true).beatable(card('cuori', '2'), foes), false); }); test('il socio ritorna il seme a chi ha bussato', () => { const c = newTressette(8); c.current = 2; c.table = []; c.hands[2] = hand([['cuori', '4'], ['fiori', '2'], ['cuori', '7']]); c.tricks = [{ leader: 0, winner: 2, cards: [{ who: 0, card: card('cuori', '5'), signal: 'busso' }, { who: 1, card: card('cuori', '6') }, { who: 2, card: card('cuori', '3') }, { who: 3, card: card('picche', '4') }] }]; const m = choose(observation(c, 2), 'male'); 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('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') }] }]; 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)); assert.ok(runs[1].playerPercent >= runs[2].playerPercent, JSON.stringify(runs)); });