1
2
3
4
5
6
7let Ball, Paddle, Brick, PhysicsEngine, LevelManager, GameController, GameState;
8
9if (typeof require !== 'undefined') {
10 const game = require('./game.js');
11 Ball = game.Ball;
12 Paddle = game.Paddle;
13 Brick = game.Brick;
14 PhysicsEngine = game.PhysicsEngine;
15 LevelManager = game.LevelManager;
16 GameController = game.GameController;
17 GameState = game.GameState;
18} else {
19 Ball = window.Ball;
20 Paddle = window.Paddle;
21 Brick = window.Brick;
22 PhysicsEngine = window.PhysicsEngine;
23 LevelManager = window.LevelManager;
24 GameController = window.GameController;
25 GameState = window.GameState;
26}
27
28
29class TestRunner {
30 constructor() {
31 this.passed = 0;
32 this.failed = 0;
33 this.tests = [];
34 this.currentSuite = '';
35 }
36
37 suite(name, fn) {
38 this.currentSuite = name;
39 fn();
40 }
41
42 test(description, fn) {
43 try {
44 fn();
45 this.passed++;
46 this.tests.push({ suite: this.currentSuite, name: description, status: 'PASS' });
47 this.log(` [PASS] ${description}`, 'green');
48 } catch (err) {
49 this.failed++;
50 this.tests.push({ suite: this.currentSuite, name: description, status: 'FAIL', error: err.message });
51 this.log(` [FAIL] ${description} -> ${err.message}`, 'red');
52 }
53 }
54
55 assert(condition, message = 'Assertion failed') {
56 if (!condition) {
57 throw new Error(message);
58 }
59 }
60
61 assertEqual(actual, expected, message) {
62 if (actual !== expected) {
63 throw new Error(message || `Expected ${expected}, but got ${actual}`);
64 }
65 }
66
67 assertInDelta(actual, expected, delta = 0.001, message) {
68 if (Math.abs(actual - expected) > delta) {
69 throw new Error(message || `Expected ${expected} (+/- ${delta}), but got ${actual}`);
70 }
71 }
72
73 log(msg, color = 'normal') {
74 if (typeof process !== 'undefined' && process.stdout) {
75 const colors = { red: '\x1b[31m', green: '\x1b[32m', cyan: '\x1b[36m', reset: '\x1b[0m' };
76 console.log(`${colors[color] || ''}${msg}${colors.reset}`);
77 } else {
78 console.log(msg);
79 }
80 }
81
82 report() {
83 this.log(`\n==========================================`, 'cyan');
84 this.log(`TEST SUMMARY: ${this.passed} PASSED, ${this.failed} FAILED`, this.failed === 0 ? 'green' : 'red');
85 this.log(`==========================================\n`, 'cyan');
86 return this.failed === 0;
87 }
88}
89
90const runner = new TestRunner();
91
92
93
94
95
96runner.suite('Ball Mechanics', () => {
97 runner.test('Ball initializes at specified position', () => {
98 const ball = new Ball(200, 300, 8, 5);
99 runner.assertEqual(ball.x, 200);
100 runner.assertEqual(ball.y, 300);
101 runner.assertEqual(ball.radius, 8);
102 runner.assertEqual(ball.stuckToPaddle, true);
103 });
104
105 runner.test('Ball moves according to velocity when launched', () => {
106 const ball = new Ball(400, 500, 7, 6);
107 ball.launch(0);
108 runner.assertEqual(ball.stuckToPaddle, false);
109 const startY = ball.y;
110 ball.update(1);
111 runner.assertInDelta(ball.y, startY - 6, 0.01, 'Ball should move 6 units upward');
112 });
113
114 runner.test('Ball speed scaling alters velocity vector correctly', () => {
115 const ball = new Ball(400, 500, 7, 6);
116 ball.launch(0);
117 ball.setSpeed(10);
118 runner.assertInDelta(ball.getSpeed(), 10, 0.001);
119 });
120});
121
122
123
124
125
126runner.suite('Paddle Mechanics', () => {
127 runner.test('Paddle clamps at left canvas boundary', () => {
128 const paddle = new Paddle(800, 600);
129 paddle.setPosition(-50, 800);
130 runner.assertEqual(paddle.x, 0, 'Paddle position should be clamped to left wall (0)');
131 });
132
133 runner.test('Paddle clamps at right canvas boundary', () => {
134 const paddle = new Paddle(800, 600);
135 paddle.setPosition(1000, 800);
136 runner.assertEqual(paddle.x, 800 - paddle.width, 'Paddle right edge should clamp at canvas right wall');
137 });
138
139 runner.test('Paddle moves smoothly left and right', () => {
140 const paddle = new Paddle(800, 600);
141 const startX = paddle.x;
142 paddle.moveRight();
143 paddle.update(800);
144 runner.assert(paddle.x > startX, 'Paddle should move right');
145 paddle.moveLeft();
146 paddle.moveLeft();
147 paddle.update(800);
148 runner.assert(paddle.x < startX + paddle.speed, 'Paddle should move left');
149 });
150});
151
152
153
154
155
156runner.suite('Wall Collision Physics', () => {
157 runner.test('Left wall bounce reverses horizontal velocity', () => {
158 const ball = new Ball(5, 300, 7, 6);
159 ball.vx = -5;
160 ball.vy = -3;
161 ball.stuckToPaddle = false;
162
163 const res = PhysicsEngine.checkWallCollision(ball, 800, 600);
164 runner.assert(res.bounced, 'Wall collision should be registered');
165 runner.assert(ball.vx > 0, 'Horizontal velocity should be positive after left wall hit');
166 runner.assertEqual(ball.x, ball.radius, 'Ball position should be reset to radius');
167 });
168
169 runner.test('Right wall bounce reverses horizontal velocity', () => {
170 const ball = new Ball(796, 300, 7, 6);
171 ball.vx = 5;
172 ball.vy = -3;
173 ball.stuckToPaddle = false;
174
175 const res = PhysicsEngine.checkWallCollision(ball, 800, 600);
176 runner.assert(res.bounced, 'Wall collision should be registered');
177 runner.assert(ball.vx < 0, 'Horizontal velocity should be negative after right wall hit');
178 });
179
180 runner.test('Top wall bounce reverses vertical velocity', () => {
181 const ball = new Ball(400, 5, 7, 6);
182 ball.vx = 2;
183 ball.vy = -6;
184 ball.stuckToPaddle = false;
185
186 const res = PhysicsEngine.checkWallCollision(ball, 800, 600);
187 runner.assert(res.bounced, 'Top wall collision should be registered');
188 runner.assert(ball.vy > 0, 'Vertical velocity should be positive after top wall hit');
189 });
190
191 runner.test('Bottom boundary detects out of bounds life loss', () => {
192 const ball = new Ball(400, 610, 7, 6);
193 ball.stuckToPaddle = false;
194
195 const res = PhysicsEngine.checkWallCollision(ball, 800, 600);
196 runner.assert(res.outOfBounds, 'Should mark outOfBounds when dropping past floor');
197 });
198});
199
200
201
202
203
204runner.suite('Paddle Collision Deflection', () => {
205 runner.test('Paddle center hit deflects ball upward with low horizontal angle', () => {
206 const paddle = new Paddle(800, 600);
207 const ball = new Ball(paddle.x + paddle.width / 2, paddle.y - 2, 7, 6);
208 ball.vx = 0;
209 ball.vy = 6;
210 ball.stuckToPaddle = false;
211
212 const hit = PhysicsEngine.checkPaddleCollision(ball, paddle);
213 runner.assert(hit, 'Paddle collision should be detected');
214 runner.assert(ball.vy < 0, 'Ball should deflect upward');
215 runner.assertInDelta(ball.vx, 0, 0.5, 'Center hit should result in near 0 horizontal velocity');
216 });
217
218 runner.test('Paddle left edge hit deflects ball to the left', () => {
219 const paddle = new Paddle(800, 600);
220 const ball = new Ball(paddle.x + 5, paddle.y - 2, 7, 6);
221 ball.vx = 0;
222 ball.vy = 6;
223 ball.stuckToPaddle = false;
224
225 const hit = PhysicsEngine.checkPaddleCollision(ball, paddle);
226 runner.assert(hit, 'Paddle collision should be detected');
227 runner.assert(ball.vy < 0, 'Ball should deflect upward');
228 runner.assert(ball.vx < 0, 'Left edge hit should deflect ball to the left');
229 });
230
231 runner.test('Paddle right edge hit deflects ball to the right', () => {
232 const paddle = new Paddle(800, 600);
233 const ball = new Ball(paddle.x + paddle.width - 5, paddle.y - 2, 7, 6);
234 ball.vx = 0;
235 ball.vy = 6;
236 ball.stuckToPaddle = false;
237
238 const hit = PhysicsEngine.checkPaddleCollision(ball, paddle);
239 runner.assert(hit, 'Paddle collision should be detected');
240 runner.assert(ball.vy < 0, 'Ball should deflect upward');
241 runner.assert(ball.vx > 0, 'Right edge hit should deflect ball to the right');
242 });
243});
244
245
246
247
248
249runner.suite('Brick Collision & HP System', () => {
250 runner.test('1-HP Brick is destroyed on first hit', () => {
251 const brick = new Brick(1, 100, 100, 60, 20, 1, '#00f3ff', 20);
252 const ball = new Ball(130, 95, 7, 6);
253 ball.vx = 0;
254 ball.vy = 6;
255 ball.stuckToPaddle = false;
256
257 const hitResult = PhysicsEngine.checkBrickCollision(ball, brick);
258 runner.assert(hitResult !== null, 'Brick collision should be registered');
259 runner.assertEqual(hitResult.destroyed, true, 'Brick should be destroyed');
260 runner.assertEqual(brick.active, false, 'Brick active state should be false');
261 });
262
263 runner.test('Multi-HP Brick reduces HP before being destroyed', () => {
264 const brick = new Brick(1, 100, 100, 60, 20, 3, '#ff007f', 50);
265 const ball = new Ball(130, 95, 7, 6);
266 ball.vx = 0;
267 ball.vy = 6;
268 ball.stuckToPaddle = false;
269
270 const hit1 = PhysicsEngine.checkBrickCollision(ball, brick);
271 runner.assert(hit1 !== null, 'First hit registered');
272 runner.assertEqual(hit1.destroyed, false, 'Brick should survive first hit');
273 runner.assertEqual(brick.hp, 2, 'Brick HP should drop to 2');
274 runner.assertEqual(brick.active, true, 'Brick should remain active');
275 });
276});
277
278
279
280
281
282runner.suite('Game Flow & Levels', () => {
283 runner.test('Level generator creates correct brick layout matrix', () => {
284 const level1 = LevelManager.createLevel(1, 800);
285 runner.assert(level1.bricks.length > 0, 'Bricks should be created for level 1');
286 runner.assert(level1.baseSpeed >= 6, 'Level 1 base speed set correctly');
287 });
288
289 runner.test('GameController score and high score management', () => {
290 const game = new GameController();
291 game.score = 500;
292 game.highScore = 300;
293 const isNewHigh = game.saveHighScore();
294 runner.assertEqual(isNewHigh, true, 'Should declare new high score');
295 runner.assertEqual(game.highScore, 500, 'High score updated to 500');
296 });
297});
298
299
300const success = runner.report();
301
302if (typeof process !== 'undefined' && process.exit) {
303 process.exit(success ? 0 : 1);
304}
305
306if (typeof window !== 'undefined') {
307 window.testRunner = runner;
308}
309
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.