Circolo 4 Maggio: dal portone in piazza si entra nella sala dei biliardi (sfondo in stile Lucas fatto da Andrea con Codex) e al primo tavolo si gioca a biliardo coi cinque birilli contro l'Ing. Tozzi (battute provvisorie, sprite da fare). Motore nuovo: fisica delle biglie e delle sponde, birilli che cadono, regole e falli del gioco italiano, avversario che prova i colpi a vuoto un po' per fotogramma; disegno dall'alto con stecca, linea di mira e biglia fantasma; stessi comandi delle bocce (pannello, tastiera, pressione); suoni; test; nelle idee la foto per i rigori
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { newBiliardo, update, press, release, setPower, shoot, judge, value, reach, speedFor, predict, castle, aimScreen, canThrow, LEN, WID, R, VIEW, SWING, powerAt } from '../src/engine/biliardo.js';
|
||||
import { story } from '../src/story/story.js';
|
||||
import { Game } from '../src/engine/game.js';
|
||||
import { settle } from './helpers.mjs';
|
||||
|
||||
const run = (c, seconds, dt = 1 / 60) => { for (let t = 0; t < seconds; t += dt) update(c, dt); };
|
||||
const still = c => { for (let i = 0; i < 3000 && !['aim', 'result'].includes(c.step); i++) update(c, 1 / 60); };
|
||||
const pins = (...which) => castle().filter((_, i) => which.includes(i));
|
||||
|
||||
test('il castello: il rosso in mezzo al tavolo, quattro bianchi a croce', () => {
|
||||
const p = castle();
|
||||
assert.equal(p.length, 5);
|
||||
assert.deepEqual(p.filter(x => x.red).map(x => [x.x, x.y]), [[LEN / 2, WID / 2]]);
|
||||
assert.equal(p.filter(x => !x.red).length, 4);
|
||||
});
|
||||
|
||||
test('il valore dei birilli: bianco 2, rosso 4, rosso da solo 10; e il fallo porta i punti all\'avversario', () => {
|
||||
assert.equal(value(pins(1)), 2);
|
||||
assert.equal(value(pins(0, 1)), 6);
|
||||
assert.equal(value(pins(0)), 10);
|
||||
assert.equal(value(pins(0, 1, 2, 3, 4)), 12);
|
||||
const by = (list, who) => list.map(p => ({ ...p, by: who }));
|
||||
assert.deepEqual(judge({ shooter: 'player', first: 'cpu', pallino: true, pins: by(pins(1, 2), 'cpu') }), { shooter: 'player', to: 'player', points: 7, foul: null });
|
||||
assert.deepEqual(judge({ shooter: 'player', first: null, pallino: false, pins: [] }), { shooter: 'player', to: 'cpu', points: 2, foul: 'miss' });
|
||||
assert.deepEqual(judge({ shooter: 'cpu', first: 'pallino', pallino: false, pins: [] }), { shooter: 'cpu', to: 'player', points: 2, foul: 'pallino' });
|
||||
assert.deepEqual(judge({ shooter: 'player', first: 'cpu', pallino: false, pins: by(pins(0), 'player') }), { shooter: 'player', to: 'cpu', points: 10, foul: 'own' });
|
||||
});
|
||||
|
||||
test('la forza: sale finché si tiene premuto; a metà la biglia fa circa un tavolo, a piena ne fa più di due', () => {
|
||||
assert.equal(powerAt(0), 0);
|
||||
assert.equal(powerAt(SWING * 2), 1);
|
||||
assert.ok(reach(speedFor(0.5)) > LEN * 0.9 && reach(speedFor(0.5)) < LEN * 1.3, `a metà forza: ${reach(speedFor(0.5)).toFixed(0)} cm`);
|
||||
assert.ok(reach(speedFor(1)) > LEN * 2);
|
||||
assert.ok(reach(speedFor(0)) < 30, 'piano piano si sposta appena');
|
||||
});
|
||||
|
||||
test('un colpo dritto sulla biglia avversaria la manda avanti, poi tocca all\'altro', () => {
|
||||
const c = newBiliardo(3);
|
||||
const me = c.balls.player, other = c.balls.cpu, before = { ...other };
|
||||
c.angle = Math.atan2(other.y - me.y, other.x - me.x);
|
||||
assert.ok(setPower(c, 0.5) && shoot(c));
|
||||
assert.equal(c.step, 'rolling');
|
||||
still(c);
|
||||
assert.ok(other.x !== before.x || other.y !== before.y, 'la biglia avversaria si è mossa');
|
||||
assert.equal(c.current, 'cpu');
|
||||
assert.equal(c.last.foul, null);
|
||||
assert.ok(c.last.points >= 0);
|
||||
});
|
||||
|
||||
test('mancare la biglia avversaria è fallo: due punti all\'altro', () => {
|
||||
const c = newBiliardo(5);
|
||||
c.balls.player = { role: 'player', x: 30, y: 20, vx: 0, vy: 0 };
|
||||
c.balls.cpu = { role: 'cpu', x: 250, y: 120, vx: 0, vy: 0 };
|
||||
c.balls.pallino = { role: 'pallino', x: 250, y: 30, vx: 0, vy: 0 };
|
||||
c.angle = 0; // dritto lungo la sponda, lontano da tutto
|
||||
assert.ok(setPower(c, 0.1) && shoot(c));
|
||||
still(c);
|
||||
assert.deepEqual(c.points, { player: 0, cpu: 2 });
|
||||
assert.equal(c.last.foul, 'miss');
|
||||
});
|
||||
|
||||
test('un birillo abbattuto dalla biglia avversaria vale punti; poi i birilli si rialzano', () => {
|
||||
const c = newBiliardo(9);
|
||||
// La mia biglia piazzata dietro a quella avversaria, in linea col castello.
|
||||
c.balls.cpu = { role: 'cpu', x: LEN / 2 - 30, y: WID / 2, vx: 0, vy: 0 };
|
||||
c.balls.player = { role: 'player', x: LEN / 2 - 60, y: WID / 2, vx: 0, vy: 0 };
|
||||
c.balls.pallino = { role: 'pallino', x: 20, y: 20, vx: 0, vy: 0 };
|
||||
c.angle = 0;
|
||||
assert.ok(setPower(c, 0.5) && shoot(c));
|
||||
for (let i = 0; i < 3000 && c.step === 'rolling'; i++) update(c, 1 / 60);
|
||||
assert.ok(c.pins.some(p => p.down), 'qualche birillo è caduto');
|
||||
assert.ok(c.pins.filter(p => p.down).every(p => p.by === 'cpu'));
|
||||
still(c);
|
||||
assert.equal(c.last.to, 'player');
|
||||
assert.ok(c.points.player >= 2, `punti: ${c.points.player}`);
|
||||
assert.ok(c.pins.every(p => !p.down), 'il castello è di nuovo in piedi');
|
||||
});
|
||||
|
||||
test('l\'avversario pensa un po\' per fotogramma, tira da solo, e la partita finisce a venti', () => {
|
||||
const c = newBiliardo(42, { target: 6 });
|
||||
c.current = 'cpu';
|
||||
let frames = 0;
|
||||
while (c.step === 'aim' && frames++ < 600) update(c, 1 / 60);
|
||||
assert.equal(c.step, 'rolling', 'l\'avversario ha tirato');
|
||||
assert.ok(frames > 20, 'non tira subito: ci pensa');
|
||||
for (let turn = 0; turn < 60 && c.step !== 'result'; turn++) {
|
||||
still(c);
|
||||
if (c.step === 'aim' && c.current === 'player') {
|
||||
const me = c.balls.player, other = c.balls.cpu;
|
||||
c.angle = Math.atan2(other.y - me.y, other.x - me.x);
|
||||
press(c); run(c, 0.6); release(c);
|
||||
}
|
||||
}
|
||||
assert.equal(c.step, 'result');
|
||||
assert.ok(['player', 'cpu'].includes(c.result.winner));
|
||||
assert.ok(c.points[c.result.winner] >= 6);
|
||||
assert.equal(press(c), false, 'a partita finita non si tira più');
|
||||
});
|
||||
|
||||
test('mirare: un tocco sullo schermo gira la stecca verso quel punto; la previsione trova la biglia davanti', () => {
|
||||
const c = newBiliardo(1);
|
||||
const me = c.balls.player;
|
||||
assert.ok(aimScreen(c, [VIEW.x + me.x * VIEW.k, VIEW.y + (me.y + 40) * VIEW.k]));
|
||||
assert.ok(Math.abs(c.angle - Math.PI / 2) < 1e-9, 'verso il basso');
|
||||
assert.equal(aimScreen(c, [VIEW.x + me.x * VIEW.k, VIEW.y + me.y * VIEW.k]), false, 'sulla biglia stessa non si mira');
|
||||
c.balls.cpu = { role: 'cpu', x: me.x + 50, y: me.y, vx: 0, vy: 0 };
|
||||
c.angle = 0;
|
||||
const p = predict(c);
|
||||
assert.equal(p.hit, 'cpu');
|
||||
assert.ok(Math.abs(p.ghost.x - (me.x + 50 - 2 * R)) < 1, 'la biglia fantasma tocca quella avversaria');
|
||||
assert.ok(canThrow(c));
|
||||
});
|
||||
|
||||
test('al Circolo 4 Maggio: dal portone in piazza si entra nella sala, e al tavolo si gioca; vincere segna la bandierina', () => {
|
||||
const g = new Game(story);
|
||||
settle(g);
|
||||
g.go('piazza'); settle(g);
|
||||
assert.ok(g.act('walk', 'circolo'));
|
||||
settle(g);
|
||||
assert.equal(g.state.scene, 'circolo_4_maggio');
|
||||
assert.ok(g.act('use', 'biliardo'));
|
||||
const before = settle(g).map(s => s.text);
|
||||
assert.ok(before.some(t => t.includes('Si va a venti')));
|
||||
assert.equal(g.closeup?.id, 'biliardo');
|
||||
assert.equal(g.act('look', 'stecche'), false, 'durante la partita la scena non risponde');
|
||||
const c = g.closeup;
|
||||
assert.ok(g.closeupAim([VIEW.x + c.balls.cpu.x * VIEW.k, VIEW.y + c.balls.cpu.y * VIEW.k]), 'si mira sulla biglia avversaria');
|
||||
assert.ok(g.closeupPress());
|
||||
for (let i = 0; i < 30; i++) g.update(1 / 60);
|
||||
assert.ok(g.closeupRelease());
|
||||
assert.equal(c.step, 'rolling');
|
||||
c.step = 'result'; c.result = { winner: 'player', points: { player: 20, cpu: 4 } };
|
||||
for (let i = 0; i < 10; i++) g.update(0.25);
|
||||
assert.equal(g.closeup, c, 'il tavolo resta a schermo finché non si esce');
|
||||
assert.ok(g.closeupClose());
|
||||
const after = settle(g).map(s => `${s.who}: ${s.text}`);
|
||||
assert.ok(after.some(t => t.startsWith('ing_tozzi: Venti. Hai vinto te')));
|
||||
assert.ok(g.state.flags.biliardo_vinto);
|
||||
// Lasciare a metà non segna niente.
|
||||
g.act('use', 'biliardo'); settle(g);
|
||||
assert.equal(g.closeup?.id, 'biliardo');
|
||||
assert.ok(g.closeupClose());
|
||||
assert.ok(settle(g).some(s => s.text.startsWith('Lasci a metà?')));
|
||||
});
|
||||
Reference in New Issue
Block a user