Files
leggenda-del-regolo/tests/pesca.test.mjs
T

186 lines
11 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { newPesca, press, release, cancel, update, cast, strike, shoot, setPower, aimScreen, nudge, status, canThrow, canCast, canAdjust,
distanceFor, biting, CASTS, LEVELS, FISHES, NEAR, FAR, BANK, SHORE, WATER, FLY, REST } from '../src/engine/pesca.js';
import { story } from '../src/story/story.js';
import { Game } from '../src/engine/game.js';
import { settle } from './helpers.mjs';
// Come il gioco: a ogni fotogramma gli eventi sonori si consumano (e qui si ricordano).
const heard = new Set();
const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) { update(c, dt); for (const e of c.events.splice(0)) heard.add(e.kind); } };
// Lancia e aspetta l'abboccata vera (non i tocchi), poi ferra: la mano ferma dei test.
function hook(c) {
assert.ok(cast(c)); run(c, FLY + 0.05);
assert.equal(c.step, 'wait');
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
assert.ok(c.float.bitten, 'prima o poi abbocca');
assert.ok(strike(c)); assert.equal(c.step, 'fight');
}
// Recupera con giudizio: tiene premuto finché la lenza non è quasi al limite, poi molla un attimo.
function land(c) {
for (let i = 0; i < 60 * 90 && c.step === 'fight'; i++) {
if (c.tension > 0.8 && c.reeling) release(c);
else if (c.tension < 0.55 && !c.reeling) press(c);
update(c, 1 / 60); for (const e of c.events.splice(0)) heard.add(e.kind);
}
assert.notEqual(c.step, 'fight', 'il recupero finisce');
}
test('la forza sale finché si tiene premuto; un tocco breve non spreca il lancio; dove si tocca l\'acqua si lancia', () => {
const c = newPesca(1);
assert.ok(press(c)); update(c, 0.05); assert.ok(release(c));
assert.equal(c.casts, 0); assert.equal(c.power, 0.5);
assert.ok(aimScreen(c, [FAR, 250])); assert.equal(c.power, 1);
assert.ok(aimScreen(c, [NEAR, 250])); assert.equal(c.power, 0);
assert.ok(!aimScreen(c, [BANK - 20, 250]), 'sulla riva non si lancia');
assert.ok(!aimScreen(c, [400, 60]), 'il cielo non è acqua');
assert.ok(nudge(c, 1)); assert.ok(Math.abs(c.power - 0.05) < 1e-9);
setPower(c, 0.5);
assert.ok(press(c)); update(c, 0.7); assert.ok(release(c));
assert.equal(c.casts, 1); assert.equal(c.step, 'cast');
assert.ok(Math.abs(c.float.x - distanceFor(c.power)) < 1e-9);
assert.ok(c.events.some(e => e.kind === 'pesca-lancio'));
assert.ok(!setPower(c, 0.2), 'a lenza in acqua la forza non si tocca');
});
test('il galleggiante: i tocchi finti abbassano appena, l\'abboccata lo porta sotto; ferrare a vuoto perde il lancio', () => {
const c = newPesca(2); cast(c); run(c, FLY + 0.05);
const f = c.float;
assert.ok(f.nibbles.length >= 1 && f.nibbles.every(n => n.at < f.biteAt), 'i tocchi vengono prima');
assert.ok(strike(c)); assert.equal(c.step, 'settle'); assert.equal(c.how, 'empty');
assert.match(c.last.text, /a vuoto|spaventato/);
run(c, REST + 0.1); assert.equal(c.step, 'aim'); assert.equal(c.casts, 1);
const d = newPesca(3); cast(d); run(d, FLY + 0.05);
const g = d.float;
for (let i = 0; i < 60 * 15 && !(g.t >= g.nibbles[0].at && g.t < g.nibbles[0].at + g.nibbles[0].for); i++) update(d, 1 / 60);
assert.ok(g.dip > 0 && g.dip < 5, `un tocco finto: ${g.dip}`);
for (let i = 0; i < 60 * 15 && !g.bitten; i++) update(d, 1 / 60);
assert.equal(g.dip, 7, 'abboccata: sotto');
assert.ok(d.events.some(e => e.kind === 'pesca-abbocca'));
});
test('non ferrata in tempo, l\'abboccata passa; alla terza il pesce se ne va', () => {
const c = newPesca(4, { level: 'male' }); cast(c); run(c, FLY + 0.05);
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
// Il segnale e la finestra coincidono: finché dice «Ferra!» si ferra; dopo, lo sta sputando.
assert.ok(biting(c) && c.float.dip === 7 && /Ferra!/.test(status(c)));
run(c, c.skill.window + 0.05);
assert.ok(!biting(c) && c.float.bitten && c.float.dip === 3 && /sputando/.test(status(c)));
const late = structuredClone(c); strike(late);
assert.equal(late.last.kind, 'late'); assert.match(late.last.text, /tardi/);
run(c, 0.5);
assert.ok(!c.float.bitten && c.float.missed === 1, 'persa la prima');
assert.equal(c.step, 'wait');
assert.match(status(c), /2 abboccate ancora/);
run(c, 60);
assert.equal(c.how, 'empty', 'dopo tre, o dopo quaranta secondi, si ricupera a vuoto');
assert.equal(c.last.kind, 'gone'); assert.equal(c.step, 'aim');
});
test('ferrato in tempo si combatte: tenendo premuto la lenza tira e il pesce si avvicina, mollando si allontana; troppo tesa si spezza', () => {
const c = newPesca(5, { level: 'bona' }); setPower(c, 0.5); hook(c);
assert.ok(c.fish && c.fish.dist === 1);
const x0 = c.float.x;
press(c); run(c, 1.5);
assert.ok(c.float.x < x0 && c.float.x > SHORE, `il galleggiante viene verso riva senza salti: ${x0} → ${c.float.x}`);
assert.ok(c.fish.dist < 1 && c.tension > 0.3, 'recuperando si avvicina');
const d0 = c.fish.dist;
release(c); run(c, 1.5);
assert.ok(c.fish.dist > d0 - 0.001, 'mollando non si avvicina');
land(c);
assert.equal(c.how, 'landed', 'con giudizio si tira a riva');
assert.equal(c.caught.length, 1); assert.ok(c.weight > 0);
assert.ok(heard.has('pesca-preso'));
// A fassi male, una carpa al largo tenuta sempre tesa spezza la lenza.
const d = newPesca(6, { level: 'male' }); setPower(d, 1); hook(d);
d.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 };
press(d); run(d, 8);
assert.equal(d.how, 'broken', `invece: ${d.step} ${d.how}`);
assert.ok(heard.has('pesca-rottura'));
});
test('un gesto solo: premendo all\'abboccata si ferra e si recupera; il pannello alterna recupera e molla; il sesto lancio si può finire', () => {
const c = newPesca(21, { level: 'bona' }); setPower(c, 0.3); cast(c); run(c, FLY + 0.05);
for (let i = 0; i < 60 * 15 && !c.float.bitten; i++) update(c, 1 / 60);
assert.ok(press(c)); assert.equal(c.step, 'fight'); assert.ok(c.reeling, 'ferrato e già in recupero');
assert.ok(release(c)); assert.ok(!c.reeling);
assert.ok(shoot(c)); assert.ok(c.reeling, 'dal pannello: recupera'); assert.ok(shoot(c)); assert.ok(!c.reeling, 'dal pannello: molla');
const d = newPesca(22); d.casts = CASTS - 1;
assert.ok(canCast(d)); cast(d); run(d, FLY + 0.05);
assert.ok(!canCast(d) && canThrow(d), 'al sesto lancio si può ancora ferrare');
assert.ok(!canAdjust(d), 'ma non regolare la distanza');
for (let i = 0; i < 60 * 15 && !d.float.bitten; i++) update(d, 1 / 60);
assert.ok(press(d)); assert.equal(d.step, 'fight'); assert.ok(canThrow(d), 'e recuperare');
d.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(d);
assert.equal(d.how, 'landed');
assert.ok(d.result, 'il risultato è deciso appena il pesce è a riva, prima della posa');
run(d, REST + 0.1); assert.equal(d.step, 'result');
});
test('la lenza si spezza davvero: tenendo sempre premuto, a seria un cavedano o più grosso la rompe, una lasca no', () => {
const fight = (kind, level) => { const c = newPesca(23, { level }); setPower(c, 0.5); hook(c); c.fish = { ...FISHES.find(f => f.kind === kind), dist: 1, energy: 1, burst: 0, reel: 0.15 }; press(c); run(c, 20); return c.how; };
assert.equal(fight('lasca', 'seria'), 'landed');
assert.equal(fight('cavedano', 'seria'), 'broken');
assert.equal(fight('carpa', 'bona'), 'broken');
assert.equal(fight('cavedano', 'bona'), 'landed');
// Mollata e ripresa, la stessa carpa viene a riva.
const c = newPesca(24, { level: 'male' }); setPower(c, 1); hook(c); c.fish = { ...FISHES.at(-1), dist: 1, energy: 1, burst: 0, reel: 0.11 }; land(c);
assert.equal(c.how, 'landed');
});
test('al largo i pesci sono più grossi; lo stivale solo sotto riva e non conta per la gloria', () => {
const near = new Set(), far = new Set();
for (let seed = 1; seed < 40; seed++) {
for (const [power, set] of [[0, near], [1, far]]) {
const c = newPesca(seed); setPower(c, power); hook(c); set.add(c.fish.kind);
}
}
assert.ok(far.has('carpa') || far.has('tinca'), `al largo: ${[...far]}`);
assert.ok(!near.has('carpa'), 'sotto riva niente carpe');
assert.ok(!far.has('stivale'), 'lo stivale sta sotto riva');
const c = newPesca(7, { level: 'bona' }); c.caught = [{ ...FISHES[0] }, { ...FISHES[1] }]; c.weight = 1;
assert.match(status(c), /Pesci 1 /, 'lo stivale non è un pesce');
const d = newPesca(25); setPower(d, 0); hook(d); d.fish = { ...FISHES[0], dist: 1, energy: 1, burst: 0, reel: 0.3 }; land(d);
assert.equal(d.weight, 0, 'e non pesa'); assert.match(d.last.text, /stivale/);
});
test('sei lanci, poi il risultato: la gloria dipende dal livello', () => {
for (const [level, { goal }] of Object.entries(LEVELS)) {
const c = newPesca(8, { level }), fishes = [];
for (let i = 0; i < CASTS; i++) {
assert.ok(canThrow(c));
// Finché serve si pesca per davvero (con una lasca docile sotto riva), poi si ricupera a vuoto.
if (c.caught.length < goal) { setPower(c, 0); hook(c); c.fish = { ...FISHES[1], dist: 1, energy: 1, burst: 0, reel: 0.2 }; land(c); assert.equal(c.how, 'landed'); }
else { cast(c); run(c, FLY + 0.05); strike(c); }
run(c, REST + 0.1);
}
assert.equal(c.step, 'result'); assert.ok(!canThrow(c)); assert.ok(!cast(c));
assert.equal(c.result.fish, goal); assert.equal(c.result.winner, 'player', level);
assert.match(status(c), /pesci su 6 lanci/);
}
const miss = newPesca(9, { level: 'bona' });
for (let i = 0; i < CASTS; i++) { cast(miss); run(miss, FLY + 0.05); strike(miss); run(miss, REST + 0.1); }
assert.equal(miss.result.winner, 'cpu');
});
test('nella storia: coi ragazzi in Fortezza «S\'ha andà a fallo?!?», loro accettano, si sceglie e si pesca; la gloria lascia il flag', () => {
const g = new Game(story);
g.go('fortezza', [240, 334]); settle(g);
g.act('talk', 'pescatori'); const said = settle(g).map(s => s.text);
assert.ok(said.some(t => t.includes('mia mamma aveva ragione')), 'la prima volta le canne');
assert.ok(g.dialogue, 'poi si parla');
g.choose(g.options().findIndex(o => o.text === 'S\'ha andà a fallo?!?'));
const yes = settle(g).map(s => `${s.who ?? 'andrea'}: ${s.text}`);
assert.ok(yes.includes('pescatori: Gnamo! La canna ti si dà noi.'));
g.choose(g.options().findIndex(o => o.text === 'Seria.')); settle(g);
assert.equal(g.closeup?.id, 'pesca'); assert.equal(g.closeup.level, 'seria');
g.closeup.result = { winner: 'player', fish: 3, weight: 2.1, goal: 3, caught: [] }; g.closeup.step = 'result';
g.closeupClose(); const after = settle(g).map(s => s.text);
assert.ok(g.state.flags.pesca_vinta);
assert.ok(after.some(t => t.includes('Stasera si frigge')));
// La seconda volta si va dritti al dialogo.
g.act('talk', 'pescatori'); settle(g);
assert.ok(g.dialogue);
});