initial Game
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Game, PHYSICS_DT, PULL_TIME } from '../public/js/game.js';
|
||||
import { segmentsCross } from '../public/js/physics.js';
|
||||
import { SHOOTER_X, LANE_LEFT } from '../public/js/table.js';
|
||||
|
||||
function seeded(seed) {
|
||||
let s = seed >>> 0;
|
||||
return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
function run(game, seconds, onStep) {
|
||||
const steps = Math.round(seconds / PHYSICS_DT);
|
||||
for (let i = 0; i < steps; i++) {
|
||||
game.step();
|
||||
if (onStep && onStep() === false) return;
|
||||
}
|
||||
}
|
||||
|
||||
function newGame(seed = 1) {
|
||||
const game = new Game({ random: seeded(seed) });
|
||||
game.setInput('launch', true); // starts the game
|
||||
game.setInput('launch', false);
|
||||
return game;
|
||||
}
|
||||
|
||||
function plunge(game, seconds) {
|
||||
game.setInput('launch', true);
|
||||
run(game, seconds);
|
||||
game.setInput('launch', false);
|
||||
}
|
||||
|
||||
test('space starts a game with the first ball in the shooter lane', () => {
|
||||
const game = new Game({ random: seeded(1) });
|
||||
assert.equal(game.state, 'attract');
|
||||
game.setInput('launch', true);
|
||||
game.setInput('launch', false);
|
||||
assert.equal(game.state, 'play');
|
||||
assert.equal(game.ballNumber, 1);
|
||||
assert.ok(game.ball.x > LANE_LEFT);
|
||||
});
|
||||
|
||||
test('pulling the plunger back never drains the ball', () => {
|
||||
const game = newGame();
|
||||
game.setInput('launch', true);
|
||||
run(game, 1.5);
|
||||
assert.equal(game.state, 'play');
|
||||
assert.equal(game.stats.drains, 0);
|
||||
});
|
||||
|
||||
test('holding space longer launches the ball harder', () => {
|
||||
const speeds = [0, 0.3, 0.6, 0.9].map((hold) => {
|
||||
const game = newGame();
|
||||
game.setInput('launch', true);
|
||||
run(game, hold);
|
||||
const pull = game.plunger.pull;
|
||||
game.setInput('launch', false);
|
||||
assert.ok(Math.abs(pull - Math.min(1, hold / PULL_TIME)) < 0.01, `pull after ${hold}s was ${pull}`);
|
||||
return -game.ball.vy;
|
||||
});
|
||||
for (let i = 1; i < speeds.length; i++) assert.ok(speeds[i] > speeds[i - 1], speeds.join(', '));
|
||||
});
|
||||
|
||||
test('every plunger strength gets the ball out of the shooter lane', () => {
|
||||
for (const hold of [0, 0.2, 0.45, 0.9]) {
|
||||
const game = newGame();
|
||||
plunge(game, hold);
|
||||
run(game, 1.5, () => !game.inPlay);
|
||||
assert.equal(game.inPlay, true, `hold ${hold}s`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a ball dropped on a raised flipper cradles, and flipping sends it up the table', () => {
|
||||
const game = newGame();
|
||||
game.setInput('left', true);
|
||||
run(game, 0.1);
|
||||
game.ball.place(190, 820);
|
||||
game.inPlay = true;
|
||||
run(game, 3);
|
||||
assert.ok(game.ball.speed < 20, 'ball should come to rest');
|
||||
assert.equal(game.state, 'play');
|
||||
game.setInput('left', false);
|
||||
run(game, 0.25);
|
||||
game.setInput('left', true);
|
||||
let highest = Infinity;
|
||||
run(game, 1.2, () => {
|
||||
highest = Math.min(highest, game.ball.y);
|
||||
});
|
||||
assert.ok(highest < 300, `ball only reached y=${highest}`);
|
||||
});
|
||||
|
||||
test('three drains end the game and the bonus is paid per ball', () => {
|
||||
const game = newGame();
|
||||
for (let ballNumber = 1; ballNumber <= 3; ballNumber++) {
|
||||
assert.equal(game.ballNumber, ballNumber);
|
||||
game.bonus = 2000;
|
||||
game.multiplier = 3;
|
||||
const before = game.score;
|
||||
game.ball.place(238, 900); // straight down the middle
|
||||
game.inPlay = true;
|
||||
run(game, 4, () => game.state === 'play' || game.state === 'bonus');
|
||||
assert.equal(game.score - before, 6000);
|
||||
}
|
||||
assert.equal(game.state, 'gameover');
|
||||
assert.equal(game.highScore, 18000);
|
||||
game.setInput('launch', true);
|
||||
assert.equal(game.state, 'play');
|
||||
assert.equal(game.score, 0);
|
||||
});
|
||||
|
||||
test('completing the top lanes raises the bonus multiplier', () => {
|
||||
const game = newGame();
|
||||
game.inPlay = true;
|
||||
game.skillShotLive = false;
|
||||
for (const r of game.table.rollovers.filter((r) => r.kind === 'top')) game.hitRollover(r);
|
||||
assert.equal(game.multiplier, 2);
|
||||
assert.deepEqual(game.topLit, [false, false, false]);
|
||||
});
|
||||
|
||||
test('the flipper buttons shift the lit top lanes', () => {
|
||||
const game = newGame();
|
||||
game.topLit = [true, false, false];
|
||||
game.setInput('right', true);
|
||||
assert.deepEqual(game.topLit, [false, true, false]);
|
||||
game.setInput('right', true); // holding the button does not shift again
|
||||
assert.deepEqual(game.topLit, [false, true, false]);
|
||||
game.setInput('left', true);
|
||||
assert.deepEqual(game.topLit, [true, false, false]);
|
||||
game.setInput('left', false);
|
||||
game.setInput('left', true);
|
||||
assert.deepEqual(game.topLit, [false, false, true]);
|
||||
});
|
||||
|
||||
test('the flippers steer the blinking skill-shot lane while it is live', () => {
|
||||
const game = newGame();
|
||||
game.skillShotLane = 1;
|
||||
game.setInput('left', true);
|
||||
assert.equal(game.skillShotLane, 0);
|
||||
game.setInput('left', false);
|
||||
game.setInput('right', true);
|
||||
assert.equal(game.skillShotLane, 1);
|
||||
});
|
||||
|
||||
test('a soft plunge usually drops into the top lanes and a full plunge orbits', () => {
|
||||
const firstSwitch = (hold) => {
|
||||
const game = newGame();
|
||||
plunge(game, hold);
|
||||
game.takeEvents();
|
||||
let first = null;
|
||||
run(game, 8, () => {
|
||||
for (const e of game.takeEvents()) {
|
||||
if (first) break;
|
||||
if (e.type === 'rollover') first = e.kind;
|
||||
else if (['bumper', 'kicker', 'sling', 'drop', 'standup', 'drain'].includes(e.type)) first = e.type;
|
||||
}
|
||||
return first === null;
|
||||
});
|
||||
return first;
|
||||
};
|
||||
let lanes = 0;
|
||||
let drains = 0;
|
||||
let count = 0;
|
||||
for (let ms = 0; ms <= 500; ms += 20, count++) {
|
||||
const first = firstSwitch(ms / 1000);
|
||||
if (first === 'top') lanes++;
|
||||
if (first === 'drain') drains++;
|
||||
}
|
||||
assert.ok(lanes / count >= 0.7, `only ${lanes}/${count} soft plunges reached a top lane`);
|
||||
assert.equal(drains, 0);
|
||||
// A full plunge sends the ball all the way around the orbit and down the outer lane, where it meets
|
||||
// the drop-target bank (flanking the pop bumpers) before reaching the kicker bulge further down.
|
||||
assert.equal(firstSwitch(0.9), 'drop');
|
||||
});
|
||||
|
||||
test('knocking down every drop target lights super bumpers and the bank resets', () => {
|
||||
const game = newGame();
|
||||
game.ball.place(300, 300);
|
||||
for (const d of game.table.dropTargets) game.hitDropTarget(d);
|
||||
assert.ok(game.superBumpers > 0);
|
||||
run(game, 2);
|
||||
assert.ok(game.table.dropTargets.every((d) => !d.down));
|
||||
});
|
||||
|
||||
function shootScoop(game) {
|
||||
game.table.lock.cooldown = 0; // the scoop briefly refuses back-to-back hits; tests fire it on demand
|
||||
const ball = game.ball;
|
||||
ball.x = game.table.lock.x;
|
||||
ball.y = game.table.lock.y;
|
||||
ball.vx = 0;
|
||||
ball.vy = 1000; // comfortably above the scoop's capture speed
|
||||
run(game, 0.05);
|
||||
}
|
||||
|
||||
test('locking a ball does not count as a drain or advance the ball number', () => {
|
||||
const game = newGame();
|
||||
const ballNumberBefore = game.ballNumber;
|
||||
shootScoop(game);
|
||||
assert.equal(game.locked, 1);
|
||||
assert.equal(game.ballNumber, ballNumberBefore);
|
||||
assert.equal(game.stats.drains, 0);
|
||||
assert.equal(game.state, 'play');
|
||||
assert.equal(game.balls.length, 1); // the locked ball was removed and a fresh one served
|
||||
assert.ok(game.ball.x > LANE_LEFT, 'the fresh ball should be back on the plunger');
|
||||
});
|
||||
|
||||
test('locking two balls then shooting the scoop a third time starts 3-ball multiball', () => {
|
||||
const game = newGame();
|
||||
shootScoop(game);
|
||||
shootScoop(game);
|
||||
assert.equal(game.locked, 2);
|
||||
shootScoop(game);
|
||||
assert.equal(game.state, 'multiball');
|
||||
assert.equal(game.locked, 0);
|
||||
assert.equal(game.balls.length, 3);
|
||||
assert.equal(game.jackpot.left, true);
|
||||
assert.equal(game.jackpot.right, true);
|
||||
assert.equal(game.jackpot.super, false);
|
||||
assert.equal(game.stats.multiballs, 1);
|
||||
for (const ball of game.balls) {
|
||||
assert.ok(ball.x >= 0 && ball.x <= 514, `spawned ball should be on the table: x=${ball.x}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a ramp shot during multiball collects its jackpot, and collecting both lights the super jackpot', () => {
|
||||
const game = newGame();
|
||||
game.state = 'multiball';
|
||||
const ball = game.spawnBall(300, 500);
|
||||
game.balls = [ball];
|
||||
game.jackpot.left = true;
|
||||
game.jackpot.right = true;
|
||||
game.jackpot.value = 25000;
|
||||
const before = game.score;
|
||||
|
||||
const leftTrack = game.table.ramps.find((r) => r.track.side === 'left').track;
|
||||
ball.track = leftTrack;
|
||||
ball.s = leftTrack.length - 1;
|
||||
ball.v = 500;
|
||||
run(game, 0.02);
|
||||
assert.equal(game.jackpot.left, false);
|
||||
assert.equal(game.score - before, 25000);
|
||||
assert.equal(game.jackpot.super, false); // only one side collected so far
|
||||
|
||||
const rightTrack = game.table.ramps.find((r) => r.track.side === 'right').track;
|
||||
ball.track = rightTrack;
|
||||
ball.s = rightTrack.length - 1;
|
||||
ball.v = 500;
|
||||
run(game, 0.02);
|
||||
assert.equal(game.jackpot.right, false);
|
||||
assert.equal(game.jackpot.super, true);
|
||||
});
|
||||
|
||||
test('collecting the super jackpot serves a fresh ball and relights both jackpots', () => {
|
||||
const game = newGame();
|
||||
game.state = 'multiball';
|
||||
const ball = game.spawnBall(game.table.lock.x, game.table.lock.y);
|
||||
game.balls = [ball];
|
||||
game.jackpot.super = true;
|
||||
game.jackpot.left = false;
|
||||
game.jackpot.right = false;
|
||||
ball.vx = 0;
|
||||
ball.vy = 1000;
|
||||
const before = game.score;
|
||||
run(game, 0.05);
|
||||
assert.equal(game.jackpot.super, false);
|
||||
assert.equal(game.jackpot.left, true);
|
||||
assert.equal(game.jackpot.right, true);
|
||||
assert.equal(game.score - before, 100000);
|
||||
assert.equal(game.balls.length, 1); // the old ball was removed and a fresh one served
|
||||
assert.notEqual(game.balls[0], ball);
|
||||
});
|
||||
|
||||
test('multiball continues until the very last ball drains, then ends as a normal ball loss', () => {
|
||||
const game = newGame();
|
||||
game.state = 'multiball';
|
||||
const a = game.spawnBall(200, 500);
|
||||
const b = game.spawnBall(300, 500);
|
||||
const c = game.spawnBall(250, 900);
|
||||
game.balls = [a, b, c];
|
||||
game.bonus = 1000;
|
||||
game.multiplier = 2;
|
||||
|
||||
a.x = 100;
|
||||
a.y = 1050;
|
||||
a.vx = 0;
|
||||
a.vy = 10;
|
||||
run(game, 0.01);
|
||||
assert.equal(game.state, 'multiball');
|
||||
assert.equal(game.balls.length, 2);
|
||||
|
||||
b.x = 100;
|
||||
b.y = 1050;
|
||||
b.vx = 0;
|
||||
b.vy = 10;
|
||||
run(game, 0.01);
|
||||
assert.equal(game.state, 'multiball'); // one ball is still in play
|
||||
assert.equal(game.balls.length, 1);
|
||||
assert.equal(game.balls[0], c);
|
||||
|
||||
c.x = 100;
|
||||
c.y = 1050;
|
||||
c.vx = 0;
|
||||
c.vy = 10;
|
||||
run(game, 0.01);
|
||||
assert.equal(game.state, 'bonus'); // the last ball of the turn: a normal drain
|
||||
assert.equal(game.balls.length, 0);
|
||||
assert.equal(game.bonusAward, 1000 * 2);
|
||||
});
|
||||
|
||||
test('long random play never tunnels through walls or escapes the table', () => {
|
||||
const game = newGame(7);
|
||||
const rand = seeded(99);
|
||||
let holdLeft = 0;
|
||||
let holdRight = 0;
|
||||
let tunnels = 0;
|
||||
for (let i = 0; i < 180 / PHYSICS_DT; i++) {
|
||||
if (game.state === 'gameover') game.setInput('launch', true), game.setInput('launch', false);
|
||||
if (
|
||||
(game.state === 'play' || game.state === 'multiball') &&
|
||||
!game.inPlay &&
|
||||
game.ball &&
|
||||
game.ball.x === SHOOTER_X &&
|
||||
Math.abs(game.ball.vy) < 1 &&
|
||||
!game.plunger.pulling
|
||||
) {
|
||||
plunge(game, rand() * 0.9);
|
||||
}
|
||||
for (const ball of game.balls) {
|
||||
for (const [index, f] of game.table.flippers.entries()) {
|
||||
if (Math.hypot(ball.x - f.x, ball.y - f.y) < 85 && ball.vy > -50 && rand() < 0.02) {
|
||||
if (index === 0) holdLeft = 0.18;
|
||||
else holdRight = 0.18;
|
||||
}
|
||||
}
|
||||
}
|
||||
holdLeft -= PHYSICS_DT;
|
||||
holdRight -= PHYSICS_DT;
|
||||
game.setInput('left', holdLeft > 0);
|
||||
game.setInput('right', holdRight > 0);
|
||||
// Snapshot each ball's track state before the step: a ball riding a ramp legitimately passes
|
||||
// over other playfield elements (that's the point of being elevated), so it's exempt below.
|
||||
const before = game.balls.map((b) => ({ ball: b, hadTrack: Boolean(b.track) }));
|
||||
game.step();
|
||||
for (const { ball: b, hadTrack } of before) {
|
||||
if (hadTrack || b.track) continue;
|
||||
for (const w of game.table.walls) {
|
||||
if (!w.enabled || !segmentsCross(b.prevX, b.prevY, b.x, b.y, w.ax, w.ay, w.bx, w.by)) continue;
|
||||
const fromBelowGate = w.tag === 'gate' && (b.prevX - w.ax) * w.nx + (b.prevY - w.ay) * w.ny < 0;
|
||||
if (!fromBelowGate) tunnels++;
|
||||
}
|
||||
}
|
||||
game.takeEvents();
|
||||
}
|
||||
assert.equal(tunnels, 0);
|
||||
assert.equal(game.stats.rescues, 0);
|
||||
assert.ok(game.stats.drains > 0);
|
||||
// Multiball itself is covered deterministically above; a random bot's luck at reaching it within any
|
||||
// one seeded run is not something this stress test should assert on.
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
Ball,
|
||||
Segment,
|
||||
Flipper,
|
||||
Track,
|
||||
collideSegment,
|
||||
collideFlipper,
|
||||
collideBalls,
|
||||
advanceOnTrack,
|
||||
resolveContact,
|
||||
makeMaterial,
|
||||
} from '../public/js/physics.js';
|
||||
|
||||
const flipperOptions = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
length: 64,
|
||||
baseRadius: 11.5,
|
||||
tipRadius: 7,
|
||||
restAngle: 0.5,
|
||||
upAngle: -0.35,
|
||||
upSpeed: 40,
|
||||
downSpeed: 16,
|
||||
material: makeMaterial(0.8, 0.43),
|
||||
};
|
||||
|
||||
// The tapered flipper is the convex hull of two discs, which equals the union of discs
|
||||
// whose centre and radius are interpolated between the two ends. Brute-force that.
|
||||
function bruteForceDistance(f, px, py) {
|
||||
const tipX = f.x + Math.cos(f.angle) * f.length;
|
||||
const tipY = f.y + Math.sin(f.angle) * f.length;
|
||||
let best = Infinity;
|
||||
for (let i = 0; i <= 4000; i++) {
|
||||
const t = i / 4000;
|
||||
const cx = f.x + (tipX - f.x) * t;
|
||||
const cy = f.y + (tipY - f.y) * t;
|
||||
const r = f.baseRadius + (f.tipRadius - f.baseRadius) * t;
|
||||
best = Math.min(best, Math.hypot(px - cx, py - cy) - r);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
test('flipper signed distance matches a brute-force distance to the tapered shape', () => {
|
||||
const f = new Flipper(flipperOptions);
|
||||
let seed = 1;
|
||||
const rand = () => ((seed = (seed * 16807) % 2147483647) / 2147483647);
|
||||
for (let i = 0; i < 300; i++) {
|
||||
const px = -40 + rand() * 140;
|
||||
const py = -60 + rand() * 120;
|
||||
const { dist } = f.distance(px, py);
|
||||
if (dist < 0.5) continue; // brute force only valid outside the shape
|
||||
assert.ok(Math.abs(dist - bruteForceDistance(f, px, py)) < 0.05, `point ${px},${py}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('flipper normal is the gradient of the distance field', () => {
|
||||
const f = new Flipper(flipperOptions);
|
||||
const h = 1e-4;
|
||||
for (const [px, py] of [[30, -20], [30, 25], [-20, 0], [75, 30], [70, 50], [5, -16]]) {
|
||||
const { dist, nx, ny } = f.distance(px, py);
|
||||
const gx = (f.distance(px + h, py).dist - dist) / h;
|
||||
const gy = (f.distance(px, py + h).dist - dist) / h;
|
||||
assert.ok(Math.abs(gx - nx) < 1e-3 && Math.abs(gy - ny) < 1e-3, `normal at ${px},${py}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a ball bounces off a wall with restitution and is pushed out of it', () => {
|
||||
const ball = new Ball(10);
|
||||
ball.place(0, -9); // 1 mm inside a floor at y = 0
|
||||
ball.vy = 2000;
|
||||
const floor = new Segment(-100, 0, 100, 0, { material: makeMaterial(0.5) });
|
||||
assert.equal(collideSegment(ball, floor), true);
|
||||
assert.equal(ball.y, -10);
|
||||
assert.ok(Math.abs(ball.vy + 1000) < 1e-9);
|
||||
});
|
||||
|
||||
test('one-way segments block from one side only', () => {
|
||||
const gate = new Segment(0, 0, 100, 0, { oneWay: true, material: makeMaterial(0.5) });
|
||||
// Left-hand normal walking a->b in screen space points up (-y): blocks balls above the line.
|
||||
const above = new Ball(10);
|
||||
above.place(50, -8);
|
||||
above.vy = 500;
|
||||
assert.equal(collideSegment(above, gate), true);
|
||||
const below = new Ball(10);
|
||||
below.place(50, 8);
|
||||
below.vy = -500;
|
||||
assert.equal(collideSegment(below, gate), false);
|
||||
});
|
||||
|
||||
test('a moving surface launches a resting ball', () => {
|
||||
const ball = new Ball(10);
|
||||
const e = 0.5;
|
||||
resolveContact(ball, 0, -1, 0, 0, -1000, makeMaterial(e));
|
||||
assert.ok(Math.abs(ball.vy + 1500) < 1e-9); // (1 + e) * surface speed
|
||||
});
|
||||
|
||||
test('equal-mass balls exchange velocity along the line of centres and are pushed apart', () => {
|
||||
const a = new Ball(13.5);
|
||||
const b = new Ball(13.5);
|
||||
a.place(0, 0);
|
||||
b.place(20, 0); // 7 mm overlap (2 * 13.5 = 27 mm needed)
|
||||
a.vx = 1000;
|
||||
const approach = collideBalls(a, b, 1); // perfectly elastic
|
||||
assert.ok(approach > 0);
|
||||
assert.ok(Math.abs(a.vx) < 1e-6, `a should stop dead: ${a.vx}`); // a transfers all its velocity to b
|
||||
assert.ok(Math.abs(b.vx - 1000) < 1e-6, `b should take a's velocity: ${b.vx}`);
|
||||
assert.ok(Math.abs(b.x - a.x - 27) < 1e-6, 'balls should be pushed apart to just touch');
|
||||
});
|
||||
|
||||
test('collideBalls does nothing when the balls are already separating', () => {
|
||||
const a = new Ball(13.5);
|
||||
const b = new Ball(13.5);
|
||||
a.place(0, 0);
|
||||
b.place(20, 0);
|
||||
a.vx = -500; // moving away from b
|
||||
const approach = collideBalls(a, b, 1);
|
||||
assert.equal(approach, 0);
|
||||
assert.equal(a.vx, -500);
|
||||
});
|
||||
|
||||
test('a straight, flat track carries a ball from start to end at constant speed', () => {
|
||||
const track = new Track([
|
||||
[0, 0, 0],
|
||||
[1000, 0, 0],
|
||||
]);
|
||||
const ball = new Ball(13.5);
|
||||
ball.track = track;
|
||||
ball.s = 0;
|
||||
ball.v = 500;
|
||||
let steps = 0;
|
||||
let result = null;
|
||||
while (!result && steps < 10000) {
|
||||
result = advanceOnTrack(ball, 1 / 1000, 0, 0, 0); // no gravity component, no friction: speed is constant
|
||||
steps++;
|
||||
}
|
||||
assert.equal(result, 'end');
|
||||
assert.ok(Math.abs(ball.v - 500) < 1, `speed should be unchanged on a flat, frictionless track: ${ball.v}`);
|
||||
assert.ok(Math.abs(ball.x - 1000) < 1);
|
||||
});
|
||||
|
||||
test('a weak shot cannot crest a steep climb and rolls back out of the entrance', () => {
|
||||
const track = new Track([
|
||||
[0, 0, 0],
|
||||
[200, 0, 100], // a short, steep 100 mm climb
|
||||
]);
|
||||
const ball = new Ball(13.5);
|
||||
ball.track = track;
|
||||
ball.s = 0;
|
||||
ball.v = 400; // too slow to climb 100 mm against strong "gravity"
|
||||
let result = null;
|
||||
for (let i = 0; i < 20000 && !result; i++) result = advanceOnTrack(ball, 1 / 1000, 1500, 9000, 100);
|
||||
assert.equal(result, 'start');
|
||||
assert.ok(ball.v < 0, `should be moving back down the entrance: ${ball.v}`);
|
||||
});
|
||||
|
||||
test('a hard shot crests the same climb and exits with reduced speed', () => {
|
||||
const track = new Track([
|
||||
[0, 0, 0],
|
||||
[200, 0, 100],
|
||||
]);
|
||||
const ball = new Ball(13.5);
|
||||
ball.track = track;
|
||||
ball.s = 0;
|
||||
ball.v = 2600;
|
||||
let result = null;
|
||||
for (let i = 0; i < 20000 && !result; i++) result = advanceOnTrack(ball, 1 / 1000, 1500, 9000, 100);
|
||||
assert.equal(result, 'end');
|
||||
assert.ok(ball.v > 0 && ball.v < 2600, `should have lost speed to the climb: ${ball.v}`);
|
||||
});
|
||||
|
||||
test('a raised flipper throws a resting ball', () => {
|
||||
const f = new Flipper(flipperOptions);
|
||||
const ball = new Ball(13.5);
|
||||
// Sit the ball on top of the flipper, halfway along.
|
||||
const along = 40;
|
||||
const c = Math.cos(f.angle);
|
||||
const s = Math.sin(f.angle);
|
||||
ball.place(c * along + s * 23, s * along - c * 23);
|
||||
f.pressed = true;
|
||||
f.update(0.001);
|
||||
assert.equal(collideFlipper(ball, f), true);
|
||||
assert.ok(ball.vy < -1000, `expected a strong upward throw, got vy=${ball.vy}`);
|
||||
});
|
||||
Reference in New Issue
Block a user